Step 1: Install and setup Laravel
Follow the below link to “Install Laravel sail & Vuejs 3.0 on MAC (M1)”.
After successfully installing laravel, we just need to change the below file.
../app/Providers/AppServiceProvider.php :
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Schema::defaultStringLength(191);
}
}
Step 2: Install tymon/jwt-auth package
Update composer.json file placed in the root directory of the project and add this line in require.
"require": {
....
....
"laravel/ui": "^3.2",
"tymon/jwt-auth": "dev-develop"
},
Update the composer from the terminal.
composer update
This command will install the package and the dependencies. Now we are going to publish the package using the below command.
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Step 3: Setup Database
Create the database using the following command.
CREATE DATABASE laravelvuedb;
Set DB details on .env file.
# create database laravelvuedb;
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=6033
DB_DATABASE=laravelvuedb
DB_USERNAME=user
DB_PASSWORD=password
Run artisan to migrate and seed into the database.
php artisan migrate
php artisan db:seed
Step 4: User interface setup in Laravel
User model file
Now open the user.php model file from inside the app folder and add the below code to it.
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
User Controller file
Create the ../app/Http/Controllers/UserController.php file and add the below code to it.
Register, login, and get authenticated user functions are defined in this controller.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Facades\JWTFactory;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Tymon\JWTAuth\JWTManager as JWT;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class UserController extends Controller
{
public function register(Request $request){
$validator = Validator::make($request->json()->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6',
]);
if($validator->fails()){
return response()->json($validator->errors()->toJson(), 400);
}
$user = User::create([
'name' => $request->json()->get('name'),
'email' => $request->json()->get('email'),
'password' => Hash::make($request->json()->get('password')),
]);
$token = JWTAuth::fromUser($user);
return response()->json(compact('user','token'), 201);
}
public function login(Request $request){
$credentials = $request->json()->all();
try {
if(! $token = JWTAuth::attempt($credentials)){
return response()->json(['error'=>'invalid Credentials'], 400);
}
}catch (JWTException $e){
return response()->json(['error'=>'could_not_create_token'], 500);
}
return response()->json(compact('token'));
}
public function getAuthenticatedUser(){
try{
if(!$user = JWTAuth::parseToken()->authenticate()){
return response()->json(['user_not_found'], 400);
}
}catch (TokenExpiredException $e){
return response()->json(['token_expired'], $e->getStatusCode());
}catch (TokenInvalidException $e){
return response()->json(['token_invalid'], $e->getStatusCode());
}catch (JWTException $e){
return response()->json(['token_absent'], $e->getStatusCode());
}
return response()->json(compact('user'));
}
}
Open ../config/app.php and add below service providers and aliases.
in providers add this in existing code.
'providers' => [
..
\Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
In aliases add this in existing code.
'aliases' => [
..
'JWTAuth' => \Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => \Tymon\JWTAuth\Facades\JWTFactory::class,
],
Step 5: Create Routes
To create the routes open routes file located here ../routes/web.php and add below routes in it.
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
After creating web routes, now we can open the project in the browser to check if that is working.
Now open the API routes file located here ../routes/api.php and add the below routes in it.
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Route::post('register', [UserController::class, 'register']);
Route::post('login', [UserController::class, 'login']);
Route::get('profile', [UserController::class, 'getAuthenticatedUser']);
After creating API routes, now we can check the created APIs using postman.
Step 6: JWT key / secret
Run PHP artisan to use JWT for authentication and authorization.
php artisan key:generate
php artisan jwt:secret
(php artisan serve&) // To run on background
Check the register and login API’s on the postman. We already have defined functionality in the UserController.php file for these API’s.
Register API test
Login API test
Step 7: Install Dependencies
If you do not have the laravel UI installed for bootstrap the run below command.
composer require laravel/ui
php artisan ui vue
Make sure you have installed node, npm, and vue. If you haven’t installed it then follow the below command.
npm install && npm run dev
php artisan ui vue --auth
npm install
Step 7: Set up Vue components
In step 1, we already have set up the Vue application.
Now open package.json file from the vueapp directory.
../vueapp/package.json and add the dependencies. Add axios and bootstrap dependencies. After adding it will look like this.
"dependencies": {
"axios": "^0.21.1",
"bootstrap": "^4.4",
"component": "^1.1.0",
"vue": "^2.5.2",
"vue-router": "^3.0.1"
},
Now go to the vueapp directory.
cd client
Run the below command in the terminal. This command will install the packages that we have just added above.
npm i
Now, Open ../vueapp/config/index.js file and update the below code in it.
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
Open ../client/src/main.js file and update code same as below.
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
require('../node_modules/bootstrap/dist/css/bootstrap.css')
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
Open ../client/src/App.vue file and update code same as below.
<template>
<div id="app">
<navbar></navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar'
export default {
name: 'App',
components: {
'Navbar': Navbar
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
open file ../vueapp/src/router/index.js and add the below code in it.
import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
import Home from '../components/Home'
import Login from '../components/Login'
import Register from '../components/Register'
import Profile from '../components/Profile'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/profile',
name: 'Profile',
component: Profile
}
]
})
Here we have added the routes for vue.
Now create the component of vue inside this folder ../vueapp/src/components and create a new file for that and will add the code in it.
Home.vue
./vueapp/src/components/Home.vue
This file contains the below code in it.
<template>
<div class="container">
<div class="jumbotron mt-5">
<div class="col-sm-8 mx-auto">
<h1 class="text-center"> Welcome</h1>
</div>
</div>
</div>
</template>
<script>
</script>
Login.vue
../vueapp/src/components/Login.vue
<template>
<div class="container">
<div class="row">
<div class="col-md-6 mt-5 mx-auto">
<form v-on:submit.prevent="login">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<div class="form-group">
<label for="email"> Email Address</label>
<input type="email" v-model="email" class="form-control" name="email" id="email"
placeholder="Email Address">
</div>
<div class="form-group">
<label for="password"> Password</label>
<input type="password" v-model="password" class="form-control" name="password" id="password"
placeholder="Password">
</div>
<button class="btn btn-lg btn-primary btn-block">Sign in</button>
</form>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import router from '../router'
export default {
data () {
return {
email: '',
password: ''
}
},
methods: {
login: function () {
axios.post('http://localhost:8000/api/login',
{
email: this.email,
password: this.password
})
.then((res) => {
localStorage.setItem('usertoken', res.data.token)
this.email = ''
this.password = ''
router.push({name: 'Profile'})
})
.catch((err) => {
console.log(err)
})
this.emitMethod()
}
emitMethod () {
}
}
}
</script>
Navbar.vue
../vueapp/src/components/Navbar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark rounded">
<button class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbar1"
aria-controls="navbar1"
aria-expanded="false"
aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-md-center" id="navbar1">
<ul class="navbar-nav">
<li class="nav-item">
<router-link class="nav-link" to="/">home</router-link>
</li>
<li v-if="auth==''" class="nav-item">
<router-link class="nav-link" to="/login">Login</router-link>
</li>
<li v-if="auth==''" class="nav-item">
<router-link class="nav-link" to="/register">Register</router-link>
</li>
<li v-if="auth=='loggedin'" class="nav-item">
<router-link class="nav-link" to="/profile">Profile</router-link>
</li>
<li v-if="auth=='loggedin'" class="nav-item">
<router-link class="nav-link" href="">Logout</router-link>
</li>
</ul>
</div>
</nav>
</template>
<script>
export default {
data () {
return {
auth: '',
user: ''
}
},
methods: {
logout () {
localStorage.removeItem('usertoken')
}
},
mounted () {
}
}
</script>
Register.vue
../vueapp/src/components/Register.vue
<template>
<div class="container">
<div class="row">
<div class="col-md-6 mt-5 mx-auto">
<form v-on:submit.prevent="register">
<h1 class="h3 mb-3 font-weight-normal">Register</h1>
<div class="form-group">
<label for="first_name"> First Name</label>
<input type="text" v-model="first_name" class="form-control" name="first_name" placeholder="first name">
</div>
<div class="form-group">
<label for="last_name"> Last Name</label>
<input type="text" v-model="last_name" class="form-control" name="last_name" placeholder="last name">
</div>
<div class="form-group">
<label for="email"> Email Address</label>
<input type="email" v-model="email" class="form-control" name="email" placeholder="Email Address">
</div>
<div class="form-group">
<label for="password"> Password</label>
<input type="password" v-model="password" class="form-control" name="password" placeholder="Password">
</div>
<button class="btn btn-lg btn-primary btn-block">Register</button>
</form>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import router from '../router'
export default{
data () {
return {
first_name: '',
last_name: '',
email: '',
password: ''
}
},
methods: {
register () {
axios.post('http://localhost:8000/api/register',
{
name: this.first_name + ' ' + this.last_name,
email: this.email,
password: this.password
})
.then((res) => {
console.log(res)
router.push({name: 'Login'})
})
.catch((err) => {
console.log(err)
})
}
}
}
</script>
Profile.vue
../vueapp/src/components/Profile.vue
This file contains below code in it.
<template>
<div class="container">
<div class="jumbotron mt-5">
<div class="col-sm-8 mx-auto">
<h1 class="text-center">Profile</h1>
</div>
<table class="table col-md-6 mx-auto">
<tbody>
<tr>
<td>Name</td>
<td>{{wholename}}</td>
</tr>
<tr>
<td>Email</td>
<td>{{email}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default{
data () {
this.getUser().then(res => {
this.wholename = res.user.name
this.email = res.user.email
return res
})
return {
wholename: '',
email: ''
}
},
methods: {
getUser () {
console.log(`Bearer ${localStorage.getItem('usertoken')}`)
return axios.get('http://localhost:8000/api/profile', {
headers: {
Authorization: `Bearer ${localStorage.getItem('usertoken')}`
}
})
.then(res => {
return res.data
})
.catch(err => {
console.log(err)
})
}
}
}
</script>
After creating this file just run the project using below command.
npm run dev
it will be look like.
Home page
Register Page
Login Page
Profile Page
So we are done with the basic functionality for Registering and log in to a application using Laravel & VueJs.
Write PHPUnit Test for Laravel API end points.
Create a copy of .env file as .env.testing.
Update the test config data [Testing DB details] on .env.testing file.
APP_NAME=Laravel
APP_ENV=testing
APP_KEY=base64:vIMosF+rzAYjEHMmbI6JIkFPKrNhBeHYAxY2oS8uC0U=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_LEVEL=debug
# create database laravelvuedb;
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=6033
DB_DATABASE=test_laravelvuedb
DB_USERNAME=user
DB_PASSWORD=password
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
Now open the tests/TestCase.php file from inside the app folder and update the below code to it.
<?php
namespace Tests;
use Faker\Factory;
abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
use CreatesApplication;
protected $faker;
/**
* To generate test data using faker and seeder
*
* @return void
*/
public function setUp(): void
{
parent::setUp();
$this->artisan('migrate');
$this->artisan('db:seed');
$this->faker = Factory::create();
}
}
Now create a file tests/app/Http/Controllers/UserCase.php file from inside the app folder and add the below code to it.
<?php
/**
* Unit test for the end points used in UserController
*
* @category Test Case File
* @license Not licensed for external use
*/
class UserTest extends \Tests\TestCase
{
/**
* Create user
*/
public function testRegister()
{
$faker = $this->faker;
$parameters = [
'name' => $faker->name,
'email' => !empty($email) ? $email
: preg_replace(
'/@example\..*/',
'@mailinator.com',
$faker->unique()->safeEmail
),
'password' => $faker->password,
];
$result = $this->post("api/register", $parameters);
$this->assertJson($result->json(),'{"name":[""],"email":[""],"password":[""]}');
}
}
On terminal run the below command to execute the PHPUnit test.
./vendor/bin/phpunit tests
Result :
So now we are done with the PHPUnit test for one of the laravel API endpoint.
Test Your Vue Components Using the Jest Testing Framework
Jest is a popular JavaScript testing framework. Lets setup the tester for Vue components.
Setup and Installing Dependencies
If you don’t have the Vue CLI installed on your machine already, start by running either:
$ npm install -g @vue/cli
Run the following command to add our testing dependencies (@vue/cli-plugin-unit-jest
and @vue/test-utils
):
$ npm install @vue/cli-plugin-unit-jest @vue/test-utils
[OR]
Add test-jest to an existing application by running the below command on the terminal.
vue add unit-jest
Modify your project’s package.json file to have an entry in scripts
as follows.
"scripts": {
...
"unit": "jest --config jest.config.js --coverage",
...
},
Then, create a file test/unit/jest.config.js
with the following content:
jest.config.js
const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname, ''),
moduleFileExtensions: [
'js',
'json',
'vue'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
testPathIgnorePatterns: [
'<rootDir>/test/e2e'
],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: ['<rootDir>/test/unit/setup'],
mapCoverage: true,
coverageDirectory: '<rootDir>/test/unit/coverage',
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!src/router/index.js',
'!**/node_modules/**'
],
verbose: true,
testURL: 'http://localhost/'
}
Coding Up a Simple Test component App
Add. a file JestTest.vue
inside src/components as such:
JestTest.vue
<template>
<div class="container">
<div class="row">
<div class="col-md-6 mt-5 mx-auto">
<h3>Let us test your arithmetic.</h3>
<p>What is the sum of the two numbers?</p>
<div class="inline">
<p>{{ x1 }} + {{ x2 }} =</p> <input v-model="guess">
<button v-on:click="check">Check Answer</button>
</div>
<button v-on:click="refresh">Refresh</button>
<p>{{ message }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
x1: Math.ceil(Math.random() * 100),
x2: Math.ceil(Math.random() * 100),
guess: '',
message: ''
}
},
methods: {
check: function () {
if (this.x1 + this.x2 === parseInt(this.guess)) {
this.message = 'SUCCESS!'
} else {
this.message = 'TRY AGAIN'
}
return this.message
},
refresh: function () {
this.x1 = Math.ceil(Math.random() * 100)
this.x2 = Math.ceil(Math.random() * 100)
return 5
}
}
}
</script>
Update the Navbar.vue to have a link to the above component.
<li v-if="auth==''" class="nav-item">
<router-link class="nav-link" to="/jestTest">Jest Test</router-link>
</li>
Update the src/rouer/index.js with the below code :
import JestTest from '../components/JestTest'
Vue.use(Router)
export default new Router({
routes: [
...
{
path: '/jestTest',
name: 'JestTest',
component: JestTest
}
]
Then go ahead and run $ npm run serve
from the root directory of your project.
Now you head over to localhost:8000
in your browser and see the working app.
Testing the App with Jest
Created a file called JestTest.spec.js
. By default, jest
will catch any test files (searching recursively through folders) in your project that are named *.spec.js
or *.test.js
.
At the top of JestTest.spec.js
we’re going to import the following from @vue/test-utils
as well as our JestTest
component itself:
Here the test covered the data and methods exported on the Vue component.
import { mount } from '@vue/test-utils'
import JestTest from '@/components/JestTest.vue'
describe('JestTest', () => {
// Inspect the raw component options
it('has data', () => {
expect(typeof JestTest.data).toBe('function')
})
})
describe('Mounted App', () => {
const wrapper = mount(JestTest)
it('renders correctly with different data', async () => {
wrapper.setData({ x1: 5, x2: 10 })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('10')
})
it('button click with correct sum', () => {
wrapper.setData({ guess: '15' })
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.vm.message).toBe('SUCCESS!')
})
test('is a Vue instance', () => {
expect(wrapper).toBeTruthy()
})
test('refresh', () => {
expect(wrapper.vm.refresh()).toEqual(5)
})
})
Run $ npm run test
in your Terminal – the test should pass.