Build a Simple Blog + Multiple Image Upload with Laravel & Vue
Tags: laravel, vue, webdev
Published: 2024-03-11 08:28:56

> Psst! If you need help setting up laravel, here’s a walkthrough:

{% embed https://devjavu.space/post/the-hitchhiker-s-guide-to-laravel-setup/ %}

Image upload is one of the most essential features of any social application. Regardless of how small or large, your application might be, as long as you’re managing users' data or allowing users to manage their own data, at some point you’d need to provide the users the ability to upload pictures within your application.

In this article, I’ll run you through how to implement multiple image upload feature with VueJS and Laravel while pretending to build a blog, lol.

Here’s a preview of what we’ll be building ( Blog Café ):

![Preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrflbeaj4dzw9tlektmq.png)

> Some Side Note 📝: - You’d need a basic understanding of Javascript - We’ll be keeping it simple and just use Twitter Bootstrap - You’ll need to have a bit of experience with Laravel/PHP and VUEjs ( Vuex too ) - For this project, we’ll be using Laravel’s local driver to store files, however, for a production-level application, you might want to use other options.
---

## Requirements
Laravel `version 5.8.*`

As at the time of writing, Laravel 5.8 was the latest version, this would have changed over time. This tutorial assumes you already have a working **LAMP** setup, so we’ll just start off by scaffolding a new Laravel application. Open up your Terminal or any preferred terminal emulator, navigate into your preferred project directory, and create a new Laravel project:

```
laravel new multi_upload
```

or via composer:

```
composer create-project --prefer-dist laravel/laravel multi_upload
```

You should now have a new directory ( multi_upload ) that contains a fresh installation of Laravel.

Next, we’ll set up the database: Create a new database using any visual database design tool (MySQL workbench, SequelPro) or from the command line like so:

```
mysql -u username -p
```

Enter your MySQL password

```
MariaDB [(none)]> CREATE DATABASE multi_upload;
```

Navigate into your project directory, copy the `.env.example` file into a `.env` file open up the `.env` file.

```
cd multi_upload && cp .env.example .env
```

Replace the default database credentials with yours.

```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blogcafe
DB_USERNAME=root
DB_PASSWORD=Caser0le.
```

Don’t forget to generate your application encryption key with:

```
php artisan key:generate
```

Once we have the correct database credentials setup, we can scaffold Laravel’s user authentication.

```
php artisan make:auth && php artisan migrate
```
---

## Models
You should have a working authentication system, but before we run a migration, let’s set up our models.

> Disclaimer: My implementation would be very basic and simplified. There are more professional ways to do this and I recommend you make more research for the best ways to achieve this.

We’ll have a **Post** model with one or more images associated with it, and a **PostImage** model that is associated with a single post. We’ll also have many **Posts** associated with a Single **User** model.

```
php artisan make:model Post -m && php artisan make:model PostImage -m
```

We’ll edit the **Post** migration file to include some fields. Open the project in your preferred code editor. If you’re using VsCode, simply open it up from the terminal:

```
code .
```

Update the Post migration (database/migrations/create_posts_……php) file to contain these columns:

```
  public function up()
  {
    Schema::create('posts', function (Blueprint $table) {
      $table->bigIncrements('id');
      $table->integer('user_id')->unsigned();
      $table->string('title');
      $table->text('body');
      $table->timestamps();
    });
  }
```

We’ll also update the PostImage migration file (database/migrations/create_posts_imag_s……ph_p) to contain the following columns:

```
  public function up()
  {
    Schema::create('post_images', function (Blueprint $table) {
      $table->bigIncrements('id');
      $table->integer('post_id')->unsigned();
      $table->string('post_image_path');
      $table->string('post_image_caption')->nullable();
      $table->timestamps();
    });
  }
```

You can now run a fresh migration so all the changes made to table will reflect:

```
php artisan migrate:fresh
```

## Relationships
Relationships can be really complicated…I know, good thing is, we’re not dealing with humans here, so it doesn’t have to be. like I explained earlier. The relationship is simple:

- A User has many Posts
- A Post can only belong to one User
- A Post has many Images
- An Image can only belong to one Post

Let’s create the first relationship between the User and the Post (A User has many Posts). in your `App\User.php` file, update the code to have the relationship.

```
  public function posts()
  {
    return $this->hasMany('App\Post');
  }
```

We’ll also create the Post Model relationship to the User Model(A Post can only belong to one User) and also the relationship between the Post Model and the PostImage Model. We also want to include the fillable fields for the Post Model (while we’re here). In your `App\Post.php` add the **author** and **post_images** function to return the relationships:

```
class Post extends Model
{
    protected $fillable = ['title', 'body', 'user_id'];

    public function author()
    {
        return $this->belongsTo('App\Models\User');
    }

    public function post_images()
    {
        return $this->hasMany('App\Models\PostImage');
    }
}
```

Remember an Image can belong to only one post, we want to add this relationship in the PostImage Class, we also want to specify the fillable fields:

```
class PostImage extends Model
{
    protected $fillable = ['post_id', 'post_image_path', 'post_image_caption'];

    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}
```

Just before we start working on our controllers, we need to setup a separate disk for uploading our images. In our config/filesystem.php file, create a new object property called **uploads** under the **disks** field:

```
...

'uploads' => [
  'driver' => 'local',
  'root' => 'uploads',
  'url' => 'uploads',
],

...
```
---

## Controller
We’ll need a controller to handle the creating and fetching of posts. we can easily make a new controller using Laravel’s `make:controller` artisan command:

php artisan make:controller PostController
You should now have a PostController file in the app/Http/Controllers/ folder.

In our `PostController.php`, we’ll create two methods: one for fetching posts (**getAllPosts**) and the other for creating a new post (**createPost**). Let’s make sure we have all the necessary classes and facades we’ll be using imported at the top of the controller file:

```
use Illuminate\Http\Request;
use App\Models\Post;
use App\Models\PostImage;
use Auth;
use Storage;
use Illuminate\Support\Facades\DB;
```

## Get All Posts
First, we need to use [eloquent’s eager loading](https://laravel.com/docs/5.8/eloquent-relationships#eager-loading) to grab all the posts as well as the related images. This is possible because of the relationship we had earlier specified in the Post model. We’ll order the results by the date created in descending order so we get the most recent posts at the top.

```
$posts = Post::with('post_images')->orderBy('created_at', 'desc')->get();
```

Then return a JSON response with the queried posts.

```
return response()->json(['error' => false, 'data' => $posts]);
```

Putting it all together:

```
    public function getAllPosts()
    {
        $posts = Post::with('post_images')->orderBy('created_at', 'desc')->get();
        return response()->json(['error' => false, 'data' => $posts]);
    }
```
---

## Create Post
For the create post function, It’ll take an instance of the `Request` class as a parameter, why? cause, like I said we’ll be making ajax requests to the backend, and data from these requests, are contained in the instance of the `Request` object. We’ll also grab all of the data we need from the request object:

- The post title
- The post content
- The array of images
- The currently authenticated user

Once we have the payload from the request, we’ll run a database transaction, to perform multiple **related** queries, which is creating a post and its related images.

> We use transactions for multiple queries that are related so the database does an automatic rollback in case one related query fails.

We already used the DB Facade at the top of our controller:

```
use Illuminate\Support\Facades\DB;
```

Both queries to create a post and related images would go within the `DB::transaction` function.

```
DB::transaction(function () use ($request) {
  // Queries happen here
}
```

Within the transaction function, we grab our payload properties:

```
$user = Auth::user();
$title = $request->title;
$body = $request->body;
$images = $request->images;
```

Next, we’ll create a new post with the title, body, and the user_id:

```
$post = Post::create([
    'title' => $title,
    'body' => $body,
    'user_id' => $user->id,
]);
```

Next, we’ll store each of the images first in a specific folder then into our database. By “specific folder” I mean a folder unique to each authenticated user and the created post. Something like this:

```
/uploads/grimesbuttom@butt.com/posts/1/skyscraper.png
// store each image
foreach($images as $image) {
    $imagePath = Storage::disk('uploads')->put($user->email . '/posts/' . $post->id, $image);
    PostImage::create([
        'post_image_caption' => $title,
        'post_image_path' => '/uploads/' . $imagePath,
        'post_id' => $post->id
    ]);
}
```

Once the images have been stored, we return a JSON response to the frontend.

```
return response()->json(200);
```

Putting it all together:

```
  public function createPost(Request $request)
  {
      DB::transaction(function () use ($request) {
          $user = Auth::user();
          $title = $request->title;
          $body = $request->body;
          $images = $request->images;

          $post = Post::create([
              'title' => $title,
              'body' => $body,
              'user_id' => $user->id,
          ]);
          // store each image
          foreach($images as $image) {
              $imagePath = Storage::disk('uploads')->put($user->email . '/posts/' . $post->id, $image);
              PostImage::create([
                  'post_image_caption' => $title,
                  'post_image_path' => '/uploads/' . $imagePath,
                  'post_id' => $post->id
              ]);
          }
      });
      return response()->json(200);
  }
```
---

## Routes
For the web routes, we’ll create a route group that’ll use the `web auth middleware`. The route group will have two routes `get_all` and `create_post` — for getting all posts and creating new posts respectively. Open the `web.php` file and add these lines:

```
Route::group(['middleware' => 'auth', 'prefix' => 'post'], function () {
    Route::get('get_all', 'PostController@getAllPosts')->name('fetch_all');
    Route::post('create_post', 'PostController@createPost')->name('create_post');
});
```
---
  
## Frontend ( blade )
Let’s move over to the frontend and do a bit of work, first on the blade part. in our home.blade.php, we need to update the UI to use a 6x6 grid layout. The left grid will hold the create-post component, while the right grid will hold the list of posts. In case you missed it earlier, this is what we’re going for:

![Preview again](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z54miau7sqsnbej5lgjo.png)

Let’s update our `home.blade.php`:

```
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-6">
            <create-post />
        </div>
        <div class="col-md-6 posts-container" style="height: 35rem; overflow-y: scroll">
            <all-posts />
        </div>
    </div>
</div>
@endsection
```

We have our blade view set up properly, most of the work will go into the individual Vue components.
---

## Components ( Vue )
Before we get started with the vue components, we need to set up our VueJs environment. Laravel already comes shipped with Vue, so we won’t have to do much work setting up, thankfully. First, we’ll install all of the npm packages in our `package.json` file:

```
npm i
```

We’ll also need [element-ui](https://element.eleme.io/#/en-US/) - mostly because we need to use the dialog box and the upload component that comes with it. It saves us more work.

```
npm i element-ui -S
```

we’ll also be using Vuex for state management.

```
npm i vuex -S
```

Once you all the necessary packages installed, we’ll set up our components, store, and packages. Create two new files `CreatePost.vue` and `AllPosts.vue` in `resources/js/components`. Also, we’ll create a new folder called store in `resources/js`. In our store folder, we’ll create an index.js file to set up our Vuex store. Your directory should look something like this:

![Directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8nxzcu0ej0ydo4o2716a.png)

We’ll now register these new components in our `resources/js/app.js` file. Add this to your `app.js` file:

```
Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('create-post', require('./components/CreatePost.vue').default);
Vue.component('all-posts', require('./components/AllPosts.vue').default);
```

In the same app.js file we would also set up our Vuex store and the element-UI library. Update your app.js file like so:

```
require('./bootstrap');

window.Vue = require('vue');
import store from './store/index';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);


Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('create-post', require('./components/CreatePost.vue').default);
Vue.component('all-posts', require('./components/AllPosts.vue').default);

const app = new Vue({
    store,
    el: '#app',
});
```

## Store
Let’s work on our store (to read more about Vuex, go to the docs), we’ll set up a single mutation and action to handle fetching and updating the posts list. in our `store/index.js` file:

- We’ll import and use the Vuex module.

```
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';
```

- Next, we want to set up our state object to hold the posts' property ( an array of all posts ). We’ll then define an asynchronous getAllPosts action to handle the request to get all posts. And finally, a setPosts mutation to update the posts' property in our state.

```
export default new Vuex.Store({
  state: {
    posts: [],
  },

  actions: {
    async getAllPosts({ commit }) {
      return commit('setPosts', await api.get('/post/get_all'))
    },
  },

  mutations: {
    setPosts(state, response) {
      state.posts = response.data.data;
    },
  },
  strict: debug
});
```

Now that our store is all set up, we’ll move into the `AppPosts.vue` component to render all the created posts.

---

## View Posts
We’ll render all the created posts in individual cards inside columns and use a dialog box to view individual post details ( this is where element-ui comes in handy ). Let’s update the `AllPosts.vue` file:

```
<template>
  <div class="row">
    <div class="col-md-6" v-for="(post, i) in posts" :key=i>
      <div class="card mt-4">
        <img v-if="post.post_images.length" class="card-img-top" :src="post.post_images[0].post_image_path">
        <div class="card-body">
          <p class="card-text"><strong>{{ post.title }}</strong> <br>
            {{ truncateText(post.body) }}
          </p>
        </div>
        <button class="btn btn-success m-2" @click="viewPost(i)">View Post</button>
      </div>
    </div>
    <el-dialog v-if="currentPost" :visible.sync="postDialogVisible" width="40%">
      <span>
        <h3>{{ currentPost.title }}</h3>
        <div class="row">
          <div class="col-md-6" v-for="(img, i) in currentPost.post_images" :key=i>
            <img :src="img.post_image_path" class="img-thumbnail" alt="">
          </div>
        </div>
        <hr>
        <p>{{ currentPost.body }}</p>
      </span>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" @click="postDialogVisible = false">Okay</el-button>
      </span>
    </el-dialog>
  </div>
</template>
```

Next in our script section, we want to use Vuex `mapState` helper which generates computed getter functions for us. We’ll pass an array of strings to `mapState` with the mapped posts property. We’ll also trigger the `getAllPost` action in a `beforeMount` hook using `store.dispatch` (Alternatively, we can use the `mapAction` helper to grab the getAllPost action). We’ll then define a helper function `truncateText` to truncate long post contents and one more function `viewPost` to view a post’s detail in a dialog box.

```
<script>
import { mapState } from 'vuex';

export default {
  name: 'all-posts',
  data() {
    return {
      postDialogVisible: false,
      currentPost: '',
    };
  },
  computed: {
    ...mapState(['posts'])
  },
  beforeMount() {
    this.$store.dispatch('getAllPosts');
  },
  methods: {
    truncateText(text) {
      if (text.length > 24) {
        return `${text.substr(0, 24)}...`;
      }
      return text;
    },
    viewPost(postIndex) {
      const post = this.posts[postIndex];
      this.currentPost = post;
      this.postDialogVisible = true;
    }
  },
}
</script>
```

To bundle everything and watch our files for changes, we’ll run [Laravel Mix](https://laravel.com/docs/5.8/mix):

```
npm run watch
```

You can start your Laravel application with:
```
php artisan serve
```

Your application should be running on `localhost:8000`. Register a new user and be sure you can view your home page (of course you might not see a lot on it yet).

I’ll go ahead to use [Tinker](https://laravel.com/docs/8.x/artisan#tinker) to create some Post and related Post Images. Tinker allows you to interact with your entire Laravel application on the command line, including the Eloquent ORM, it’s an interactive PHP shell. You can access the tinker interface with:

```
php artisan tinker
```

I won’t go into all the details about using tinker, but if everything works fine you should be able to create posts and images from the tinker shell:

```
>>> $user = App\User::find(1)
>>> $post = App\Post::create([
        "title" => "Some blog post",
        "body" => "this is a random post about absolutely nothing"
        "user_id" => $user->id
    ])
>>> $postImage = App\PostImage::create([
        "post_image_caption" => $post->title,
        "post_image_path" => "https://skillsouq.com/wp-content/uploads/2014/10/background_01.jpg"
    ])
```
---

## Create Posts
To create a new post, we’ll need a form to take the post title, post content, and post images. for the image upload, we’ll be using element-ui’s upload component, this will help us handle and preview the files properly…It also has a better user experience.

We’ll update the `CreatePost.vue` component:

```
<template>
  <div class="card mt-4" :key="componentKey">
    <div class="card-header">New Post</div>
    <div class="card-body">
      <div
        v-if="status_msg"
        :class="{ 'alert-success': status, 'alert-danger': !status }"
        class="alert"
        role="alert"
      >{{ status_msg }}</div>
      <form>
        <div class="form-group">
          <label for="exampleFormControlInput1">Title</label>
          <input
            v-model="title"
            type="text"
            class="form-control"
            id="title"
            placeholder="Post Title"
            required
          />
        </div>
        <div class="form-group">
          <label for="exampleFormControlTextarea1">Post Content</label>
          <textarea v-model="body" class="form-control" id="post-content" rows="3" required></textarea>
        </div>
        <div class>
          <el-upload
            action="https://jsonplaceholder.typicode.com/posts/"
            list-type="picture-card"
            :on-preview="handlePictureCardPreview"
            :on-change="updateImageList"
            :auto-upload="false"
          >
            <i class="el-icon-plus"></i>
          </el-upload>
          <el-dialog :visible.sync="dialogVisible">
            <img width="100%" :src="dialogImageUrl" alt />
          </el-dialog>
        </div>
      </form>
    </div>
    <div class="card-footer">
      <button
        type="button"
        @click="createPost"
        class="btn btn-success"
      >{{ isCreatingPost ? "Posting..." : "Create Post" }}</button>
    </div>
  </div>
</template>
```

We’ll add some styling to the upload component:

```
<style>
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.avatar-uploader .el-upload:hover {
  border-color: #409eff;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>
```

In our script section, we’ll import the `mapAction` helper which maps component methods to store.dispatch calls.

```
import { mapActions } from 'vuex';
```

Next we’ll be needing these data properties:

```
export default {
  name: "CreatePost",
  data () {
    return {
      dialogImageUrl: '',
      dialogVisible: false,
      imageList: [],
      status_msg: '',
      status: '',
      isCreatingPost: false,
      title: '',
      body: '',
      componentKey: 0
    }
  },
  ...
```

Within our method property, we’ll map the `getAllPosts` action we defined in our store to the component:

```
methods: {
  ...mapActions(['getAllPosts']),
}
```

Next in our methods property, we’ll need a couple of methods to handle image preview and update our image list.

```
methods: {
  updateImageList (file) {
      this.imageList.push(file.raw)
  },
  handlePictureCardPreview (file) {
      this.dialogImageUrl = file.url
      this.imageList.push(file)
      this.dialogVisible = true
  },
  ...
}
```

We’ll need another method to handle showing success and error notifications.

```
...
showNotification (message) {
  this.status_msg = message
  setTimeout(() => {
    this.status_msg = ''
  }, 3000)
}
...
```

One more method to handle validation of our form… (you thought it was over, eh?)

```
...
validateForm () {
  // no vaildation for images - it is needed
  if (!this.title) {
    this.status = false
    this.showNotification('Post title cannot be empty')
    return false
  }
  if (!this.body) {
    this.status = false
    this.showNotification('Post body cannot be empty')
    return false
  }
  if (this.imageList.length < 1) {
    this.status = false;
    this.showNotification('You need to select an image');
    return false;
  }
  return true
},
...
```

And finally our `createPost` Method:

```
  createPost (e) {
    e.preventDefault()
    if (!this.validateForm()) {
      return false
    }
    const that = this
    this.isCreatingPost = true
    const formData = new FormData()

    formData.append('title', this.title)
    formData.append('body', this.body)

    // JQuery comes preinstalled with Laravel-Vue so we can do this
    $.each(this.imageList, function (key, image) {
      formData.append(`images[${key}]`, image)
    })

    window.api.post('/post/create_post', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      })
      .then((res) => {
        this.title = this.body = ''
        this.status = true
        this.showNotification('Post Successfully Created')
        this.isCreatingPost = false
        this.imageList = []
        /*
          this.getAllPosts() can be used here as well
          note: "that" has been assigned the value of "this" at the top
          to avoid context related issues.
          */
        that.getAllPosts()
        that.componentKey += 1
      })
  },
```

Because we need to send image files to our API, we’ll be using the [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) class. The FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values. This is so our request looks like it’s coming from an actual form and allow Laravel to read the image file properly.

If all went well and the gods were with us, we should have it working as expected:

![Working site](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ufexe6p8bnj7k31s455.png)


![Working site](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/my7hvttem3x7y7uufdac.png)

I know this was a long ride, hopefully, you were able to make this work on your end. The codebase for this tutorial lives on [this repository](https://github.com/MartinsOnuoha/BlogCafe) feel free to explore.

{% embed https://github.com/MartinsOnuoha/BlogCafe %}

Cheers ☕️
---