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.
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.
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.
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.
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
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.
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 forlaravel-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 thescoped
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:
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.
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.