Build an e-commerce application using Laravel and Vue Part 2: Handling all the request and operations

ecommerce-laravel-vue-part-2-header.png

This three-part tutorial series shows how to build an e-commerce application with Laravel and Vue. It includes authentication using Passport, and a simple sqlite database. In part two, implement the controller logic to handle requests to your application, and set up Vue and VueRouter.

Introduction

In the previous chapter, we set up our application’s migrations and models, and installed Laravel Passport for authentication. We also planned what the application will look like. In this chapter, we will implement the controllers and handle all requests to our application.

Prerequisites

To continue with this part, please go through the first part of the series first and make sure you have all the requirements from that part.

Building our controllers

In the first chapter, we already defined our models and their accompanying controllers. These controllers reside in the app/Http/Controllers directory. The User model, however, does not have an accompanying controller, so we are going to create that first. Run the following command:

1$ php artisan make:controller UserController

Now, open the created controller file app/Http/Controllers/UserController.php and replace the contents with the following:

1<?php
2
3    namespace App\Http\Controllers;
4
5    use Auth;
6    use App\User;
7    use Validator;
8    use Illuminate\Http\Request;
9
10    class UserController extends Controller
11    {
12        public function index()
13        {
14            return response()->json(User::with(['orders'])->get());
15        }
16
17        public function login(Request $request)
18        {
19            $status = 401;
20            $response = ['error' => 'Unauthorised'];
21
22            if (Auth::attempt($request->only(['email', 'password']))) {
23                $status = 200;
24                $response = [
25                    'user' => Auth::user(),
26                    'token' => Auth::user()->createToken('bigStore')->accessToken,
27                ];
28            }
29
30            return response()->json($response, $status);
31        }
32
33        public function register(Request $request)
34        {
35            $validator = Validator::make($request->all(), [
36                'name' => 'required|max:50',
37                'email' => 'required|email',
38                'password' => 'required|min:6',
39                'c_password' => 'required|same:password',
40            ]);
41
42            if ($validator->fails()) {
43                return response()->json(['error' => $validator->errors()], 401);
44            }
45
46            $data = $request->only(['name', 'email', 'password']);
47            $data['password'] = bcrypt($data['password']);
48
49            $user = User::create($data);
50            $user->is_admin = 0;
51
52            return response()->json([
53                'user' => $user,
54                'token' => $user->createToken('bigStore')->accessToken,
55            ]);
56        }
57
58        public function show(User $user)
59        {
60            return response()->json($user);
61        }
62
63        public function showOrders(User $user)
64        {
65            return response()->json($user->orders()->with(['product'])->get());
66        }
67
68    }

Above we defined some class methods:

  • index() – returns all users with their orders.
  • login() – authenticates a user and generates an access token for that user. The createToken method is one of the methods Laravel Passport adds to our user model.
  • register() – creates a user account, authenticates it and generates an access token for it.
  • show() – gets the details of a user and returns them.
  • showOrders() – gets all the orders of a user and returns them.

We used Laravel’s Route-Model Binding to automatically inject our model instance into the controller. The only caveat is that the variable name used for the binding has to be the same as the one defined in the route as well.

Next, open the app/Http/Controllers/ProductController.php file and replace the contents with the following:

1<?php
2
3    namespace App\Http\Controllers;
4
5    use App\Product;
6    use Illuminate\Http\Request;
7
8    class ProductController extends Controller
9    {
10        public function index()
11        {
12            return response()->json(Product::all(),200);
13        }
14
15        public function store(Request $request)
16        {
17            $product = Product::create([
18                'name' => $request->name,
19                'description' => $request->description,
20                'units' => $request->units,
21                'price' => $request->price,
22                'image' => $request->image
23            ]);
24
25            return response()->json([
26                'status' => (bool) $product,
27                'data'   => $product,
28                'message' => $product ? 'Product Created!' : 'Error Creating Product'
29            ]);
30        }
31
32        public function show(Product $product)
33        {
34            return response()->json($product,200); 
35        }
36
37        public function uploadFile(Request $request)
38        {
39            if($request->hasFile('image')){
40                $name = time()."_".$request->file('image')->getClientOriginalName();
41                $request->file('image')->move(public_path('images'), $name);
42            }
43            return response()->json(asset("images/$name"),201);
44        }
45
46        public function update(Request $request, Product $product)
47        {
48            $status = $product->update(
49                $request->only(['name', 'description', 'units', 'price', 'image'])
50            );
51
52            return response()->json([
53                'status' => $status,
54                'message' => $status ? 'Product Updated!' : 'Error Updating Product'
55            ]);
56        }
57
58        public function updateUnits(Request $request, Product $product)
59        {
60            $product->units = $product->units + $request->get('units');
61            $status = $product->save();
62
63            return response()->json([
64                'status' => $status,
65                'message' => $status ? 'Units Added!' : 'Error Adding Product Units'
66            ]);
67        }
68
69        public function destroy(Product $product)
70        {
71            $status = $product->delete();
72
73            return response()->json([
74                'status' => $status,
75                'message' => $status ? 'Product Deleted!' : 'Error Deleting Product'
76            ]);
77        }
78    }

In the ProductController above we defined seven methods:

  • index() – fetches and returns all the product records.
  • store() – creates a product record.
  • show() – fetches and returns a single product.
  • uploadFile() – uploads the image for a product we created and returns the url for the product.
  • update() – updates the product record.
  • updateUnits() – adds new units to a product.
  • delete() – deletes a product.

Next, open the app/Http/Controllers/OrderController.php file and replace the content with the following:

1<?php
2
3    namespace App\Http\Controllers;
4
5    use App\Order;
6    use Auth;
7    use Illuminate\Http\Request;
8
9    class OrderController extends Controller
10    {
11        public function index()
12        {
13            return response()->json(Order::with(['product'])->get(),200);
14        }
15
16        public function deliverOrder(Order $order)
17        {
18            $order->is_delivered = true;
19            $status = $order->save();
20
21            return response()->json([
22                'status'    => $status,
23                'data'      => $order,
24                'message'   => $status ? 'Order Delivered!' : 'Error Delivering Order'
25            ]);
26        }
27
28        public function store(Request $request)
29        {
30            $order = Order::create([
31                'product_id' => $request->product_id,
32                'user_id' => Auth::id(),
33                'quantity' => $request->quantity,
34                'address' => $request->address
35            ]);
36
37            return response()->json([
38                'status' => (bool) $order,
39                'data'   => $order,
40                'message' => $order ? 'Order Created!' : 'Error Creating Order'
41            ]);
42        }
43
44        public function show(Order $order)
45        {
46            return response()->json($order,200);
47        }
48
49        public function update(Request $request, Order $order)
50        {
51            $status = $order->update(
52                $request->only(['quantity'])
53            );
54
55            return response()->json([
56                'status' => $status,
57                'message' => $status ? 'Order Updated!' : 'Error Updating Order'
58            ]);
59        }
60
61        public function destroy(Order $order)
62        {
63            $status = $order->delete();
64
65            return response()->json([
66                'status' => $status,
67                'message' => $status ? 'Order Deleted!' : 'Error Deleting Order'
68            ]);
69        }
70    }

In the OrderController above we have six methods:

  • index() – fetches and returns all the orders.
  • deliverOrder() – marks an order as delivered.
  • store() – creates an order.
  • show() – fetches and returns a single order.
  • update() – updates the order.
  • destroy() – deletes an order.

That’s it for our controllers. We have created the controller according to the specifications we laid out in the first part. Next thing we need to do is define our API routes.

Defining our application’s routes

Now that we have fully defined all the requests we would like to make to our application, let’s expose the APIs for making these requests. Open routes/api.php file and replace the content with the following:

1<?php
2
3    use Illuminate\Http\Request;
4
5    Route::post('login', 'UserController@login');
6    Route::post('register', 'UserController@register');
7    Route::get('/products', 'ProductController@index');
8    Route::post('/upload-file', 'ProductController@uploadFile');
9    Route::get('/products/{product}', 'ProductController@show');
10
11    Route::group(['middleware' => 'auth:api'], function(){
12        Route::get('/users','UserController@index');
13        Route::get('users/{user}','UserController@show');
14        Route::patch('users/{user}','UserController@update');
15        Route::get('users/{user}/orders','UserController@showOrders');
16        Route::patch('products/{product}/units/add','ProductController@updateUnits');
17        Route::patch('orders/{order}/deliver','OrderController@deliverOrder');
18        Route::resource('/orders', 'OrderController');
19        Route::resource('/products', 'ProductController')->except(['index','show']);
20    });

Putting our route definitions in the routes/api.php file will tell Laravel they are API routes so Laravel will prefix the routes with a /api in the URL to differentiate them from web-routes.

Adding the auth:api middleware ensures any calls to the routes in that group must be authenticated.

A thing to note is, using the resource method on the Route class helps us create some additional routes under the hood without us having to create them manually. Read about resource controllers and routes here.

? To see the full route list, run the following command: $ php artisan route:list

Laravel ecommerce route list

Since we will build the front end of this application in Vue, we need to define the web routes for it. Open the routes/web.php file and replace the contents with the following:

1<?php
2
3    Route::get('/{any}', function(){
4            return view('landing');
5    })->where('any', '.*');

This will route every web request to a single entry point, which will be the entry for your Vue application.

Setting up Vue for the frontend

Vue is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable – vuejs.org

Laravel comes with Vue bundled out of the box, so all we need to do to get Vue is to install the node packages. Run the following command:

1$ npm install

Next, we will need VueRouter to handle the routing between the different components of our Vue application. To install VueRouter run the command below:

1$ npm install vue-router

Next, let’s make the landing view file, which would mount our Vue application. Create the file resources/views/landing.blade.php and add the following code:

1<!DOCTYPE html>
2    <html>
3    <head>
4        <meta charset="utf-8">
5        <meta http-equiv="X-UA-Compatible" content="IE=edge">
6        <meta name="viewport" content="width=device-width, initial-scale=1">
7        <meta name="csrf-token" content="{{csrf_token()}}">
8        <title>Big Store</title>
9        <link href=" {{ mix('css/app.css') }}" rel="stylesheet">
10    </head>
11    <body>
12        <div id="app">
13            <app></app>
14        </div>
15        <script src="{{ mix('js/bootstrap.js') }}"></script>
16        <script src="{{ mix('js/app.js') }}"></script>
17    </body>
18    </html>

In the code above, we have the HTML for our application. If you look closely, you can see the app tag. This will be the entry point to our Vue application and where the components will be loaded.

Since we will use app.js to set up our VueRouter, we still need to have Bootstrap and Axios compiled. The import for Bootstrap and Axios is in the bootstrap.js file so we need to compile that.

Edit the webpack.mix.js file so it compiles all assets:

1[...]
2
3    mix.js('resources/assets/js/app.js', 'public/js')
4       .js('resources/assets/js/bootstrap.js', 'public/js')
5       .sass('resources/assets/sass/app.scss', 'public/css');

?the webpack.mix.js file holds the configuration files for laravel-mix, which provides a wrapper around Webpack. It lets us take advantage of Webpack’s amazing asset compilation abilities without having to write Webpack configurations by ourselves. You can learn more about Webpack here.

Set up the homepage for the Vue application. Create a new file, resources/assets/js/views/Home.vue, and add the following code to the file:

1<template>
2        <div>
3            <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto">
4                <h2 class="title">Welcome to the bigStore</h2>
5            </div>
6            <div class="container">
7                <div class="row">
8                    <div class="col-md-12">
9                        <div class="row">
10                            <div class="col-md-4 product-box" v-for="(product,index) in products" @key="index">
11                                <router-link :to="{ path: '/products/'+product.id}">
12                                    <img :src="product.image" :alt="product.name">
13                                    <h5><span v-html="product.name"></span>
14                                        <span class="small-text text-muted float-right">$ {{product.price}}</span>
15                                    </h5>
16                                    <button class="col-md-4 btn btn-sm btn-primary float-right">Buy Now</button>
17                                </router-link>
18                            </div>
19                        </div>
20                    </div>
21                </div>
22            </div>
23        </div>
24    </template>
25
26    <script>
27        export default {
28            data(){
29                return {
30                    products : []
31                }
32            },
33            mounted(){
34                axios.get("api/products/").then(response => this.products = response.data)      
35            }
36        }
37    </script>

The code above within the opening and closing template tag we have the HTML of our Vue component. In there we loop through the contents of products and for each product we display the image, name, id, price and units available. We use the v-html attribute to render raw HTML, which makes it easy for us to use special characters in the product name.

In the script tag, we defined the data(), which holds all the variables we can use in our template. We also defined the mounted() method, which is called after our component is loaded. In this mounted method, we load our products from the API then set the products variable so that our template would be updated with API data.

In the same file, append the code below to the bottom:

1<style scoped>
2    .small-text {
3        font-size: 14px;
4    }
5    .product-box {
6        border: 1px solid #cccccc;
7        padding: 10px 15px;
8    }
9    .hero-section {
10        height: 30vh;
11        background: #ababab;
12        align-items: center;
13        margin-bottom: 20px;
14        margin-top: -20px;
15    }
16    .title {
17        font-size: 60px;
18        color: #ffffff;
19    }
20    </style>

In the code above, we have defined the style to use with the welcome component.

According to the Vue documentation:

When a <style> tag has the scoped attribute, its CSS will apply to elements of the current component only. This is similar to the style encapsulation found in the Shadow DOM. It comes with some caveats but doesn’t require any polyfills.

Next create another file, resources/assets/js/views/App.vue. This will be the application container where all other components will be loaded. In this file, add the following code:

1<template>
2        <div>
3            <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
4                <div class="container">
5                    <router-link :to="{name: 'home'}" class="navbar-brand">Big Store</router-link>
6                    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
7                        <span class="navbar-toggler-icon"></span>
8                    </button>
9                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
10                        <!-- Left Side Of Navbar -->
11                        <ul class="navbar-nav mr-auto"></ul>
12                        <!-- Right Side Of Navbar -->
13                        <ul class="navbar-nav ml-auto">
14                            <router-link :to="{ name: 'login' }" class="nav-link" v-if="!isLoggedIn">Login</router-link>
15                            <router-link :to="{ name: 'register' }" class="nav-link" v-if="!isLoggedIn">Register</router-link>
16                            <span v-if="isLoggedIn">
17                                <router-link :to="{ name: 'userboard' }" class="nav-link" v-if="user_type == 0"> Hi, {{name}}</router-link>
18                                <router-link :to="{ name: 'admin' }" class="nav-link" v-if="user_type == 1"> Hi, {{name}}</router-link>
19                            </span>
20                            <li class="nav-link" v-if="isLoggedIn" @click="logout"> Logout</li>
21                        </ul>
22                    </div>
23                </div>
24            </nav>
25            <main class="py-4">
26                <router-view @loggedIn="change"></router-view>
27            </main>
28        </div>
29    </template>

In the Vue template above we used some Vue specific tags like router-link, which helps us generate links for routing to pages defined in our router. We also have the router-view, which is where all the child component pages will be loaded.

Next, below the closing template tag, add the following code:

1<script>
2    export default {
3        data() {
4            return {
5                name: null,
6                user_type: 0,
7                isLoggedIn: localStorage.getItem('bigStore.jwt') != null
8            }
9        },
10        mounted() {
11            this.setDefaults()
12        },
13        methods : {
14            setDefaults() {
15                if (this.isLoggedIn) {
16                    let user = JSON.parse(localStorage.getItem('bigStore.user'))
17                    this.name = user.name
18                    this.user_type = user.is_admin
19                }
20            },
21            change() {
22                this.isLoggedIn = localStorage.getItem('bigStore.jwt') != null
23                this.setDefaults()
24            },
25            logout(){
26                localStorage.removeItem('bigStore.jwt')
27                localStorage.removeItem('bigStore.user')
28                this.change()
29                this.$router.push('/')
30            }
31        }
32    }
33    </script>

In the script definition we have the methods property and in there we have three methods defined:

  • setDefaults() – sets the name of the user when the user is logged in as well as the type of user logged in.
  • change()– checks the current login status anytime it is called and calls the setDefaults method.
  • logout() – logs the user out of the application and routes the user to the homepage.

In our router-view component, we listen for an event loggedIn which calls the change method. This event is fired by our component anytime we log in. It is a way of telling the App component to update itself when a user logs in.

Next create the following files in the resources/assets/js/views directory:

  • Admin.vue
  • Checkout.vue
  • Confirmation.vue
  • Login.vue
  • Register.vue
  • SingleProduct.vue
  • UserBoard.vue

These files would hold all the pages bigStore would have. They need to be created prior to setting up VueRouter, so that it won’t throw an error.

To set up the routing for our Vue single page app, open your resources/assets/js/app.js file and replace the contents with the following code:

1import Vue from 'vue'
2    import VueRouter from 'vue-router'
3
4    Vue.use(VueRouter)
5
6    import App from './views/App'
7    import Home from './views/Home'
8    import Login from './views/Login'
9    import Register from './views/Register'
10    import SingleProduct from './views/SingleProduct'
11    import Checkout from './views/Checkout'
12    import Confirmation from './views/Confirmation'
13    import UserBoard from './views/UserBoard'
14    import Admin from './views/Admin'
15
16    const router = new VueRouter({
17        mode: 'history',
18        routes: [
19            {
20                path: '/',
21                name: 'home',
22                component: Home
23            },
24            {
25                path: '/login',
26                name: 'login',
27                component: Login
28            },
29            {
30                path: '/register',
31                name: 'register',
32                component: Register
33            },
34            {
35                path: '/products/:id',
36                name: 'single-products',
37                component: SingleProduct
38            },
39            {
40                path: '/confirmation',
41                name: 'confirmation',
42                component: Confirmation
43            },
44            {
45                path: '/checkout',
46                name: 'checkout',
47                component: Checkout,
48                props: (route) => ({ pid: route.query.pid })
49            },
50            {
51                path: '/dashboard',
52                name: 'userboard',
53                component: UserBoard,
54                meta: {
55                    requiresAuth: true,
56                    is_user: true
57                }
58            },
59            {
60                path: '/admin/:page',
61                name: 'admin-pages',
62                component: Admin,
63                meta: {
64                    requiresAuth: true,
65                    is_admin: true
66                }
67            },
68            {
69                path: '/admin',
70                name: 'admin',
71                component: Admin,
72                meta: {
73                    requiresAuth: true,
74                    is_admin: true
75                }
76            },
77        ],
78    })
79
80    router.beforeEach((to, from, next) => {
81        if (to.matched.some(record => record.meta.requiresAuth)) {
82            if (localStorage.getItem('bigStore.jwt') == null) {
83                next({
84                    path: '/login',
85                    params: { nextUrl: to.fullPath }
86                })
87            } else {
88                let user = JSON.parse(localStorage.getItem('bigStore.user'))
89                if (to.matched.some(record => record.meta.is_admin)) {
90                    if (user.is_admin == 1) {
91                        next()
92                    }
93                    else {
94                        next({ name: 'userboard' })
95                    }
96                }
97                else if (to.matched.some(record => record.meta.is_user)) {
98                    if (user.is_admin == 0) {
99                        next()
100                    }
101                    else {
102                        next({ name: 'admin' })
103                    }
104                }
105                next()
106            }
107        } else {
108            next()
109        }
110    })

Above, we have imported the VueRouter and we added it to our Vue application. We defined routes for our application and then registered it to the Vue instance so it is available to all Vue components.

Each of the route objects has a name, which we will use to identify and invoke that route. It also has a path, which you can visit directly in your browser. Lastly, it has a component, which is mounted when you visit the route.

On some routes, we defined meta, which contains variables we would like to check when we access the route. In our case, we are checking if the route requires authentication and if it is restricted to administrators or regular users only.

We set up the beforeEach middleware on the router that checks each route before going to it. The method takes these variables:

  • to – the route you want to move to.
  • from – the current route you are moving away from.
  • next – the method that finally moves to a defined route. When called without a route passed, it continues the navigation. If given a route, it goes to that route.

We use beforeEach to check the routes that require authentication before you can access them. For those routes, we check if the user is authenticated. If the user isn’t, we send them to the login page. If the user is authenticated, we check if the route is restricted to admin users or regular users. We redirect each user to the right place based on which access level they have.

Now add the following lines to the end of the app.js file

1const app = new Vue({
2        el: '#app',
3        components: { App },
4        router,
5    });

This instantiates the Vue application. In this global instance, we mount the App component only because the VueRouter needs it to switch between all the other components.

Now, we are ready to start making the other views for our application.

Conclusion

In this part, we implemented the controller logic that handles all the requests to our application and defined all the routes the application will use. We also set up Vue and VueRouter to prepare our application for building the core frontend.

In the next chapter of this guide, we are going to build the core frontend of the application and consume the APIs. See you in the next part.