Building a “CRUD” app (Larafirebase) with Google Firebase using PHPFirebase, Laravel & VueJS.

Josiah Ovye Yahaya
Dec 1, 2018 · 11 min read

Lots of hype has been for Google Firebase. PHP however, don’t have much support on the Google Firebase platform. Today, I will be sharing with you how easy it can be building a CRUD app using PHP and Firebase.

This article will be about a simple app I built recently with PHP(Laravel) and Google Firebase. I will be taking you step by step on how I got to do it.

Meet Larafirebase — The app we will be talking about here

What we need

For every project, planing and timing is important. It’s same with this small one. Below are the things we need to achieve what we want and how fast we want to get it done.

Tools

  1. Library
  2. Laravel
  3. VueJS
  4. Any good editor or IDE (I use PHPStorm)

Time

~ 1hr 30mins.

Getting started

I started by installing and library for performing CRUD actions on .

Since I already have laravel installed globally on my machine, I just did this.

laravel new larafirebase

Next, is the installation of the library.

composer require coderatio/phpfirebase:v1.0

Because, I will be using VueJS for my front-end, I had to install all npm dependencies. I did that by using this command.

npm install

Initializing our app with VueJS

We will do this in our app.js file inside resources => js folder.

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

let Home = require('./components/Home')
let Posts = require('./components/Posts')
let EditPost = require('./components/EditPost')
let AddPost = require('./components/AddPost')
let ReadPost = require('./components/ReadPost')

const routes = [
{ path: '/', component: Home},
{ path: '/posts', component: Posts },
{ path: '/post/add', component: AddPost },
{ path: '/post/:postId/edit/', component: EditPost },
{ path: '/post/:postId/read/', component: ReadPost }
];

const router = new VueRouter({
routes
});

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

Creating our Laravel Controllers

We will be needing only two controllers for this project.

  1. WelcomeController (Responsible for handling welcome page request)
  2. PostsController (Responsible for handling all our CRUD actions requests)

Creating our Vue Components

We will create components for each of our CRUD action except the Delete and the browse component as well.

  1. CreatePost or AddPost
  2. ReadPost
  3. UpdatePost or EditPost
  4. Posts (Which is responsible for displaying all our created posts).

1. Rendering our welcome page

To render our welcome page, we need routes(Laravel & Vue), a controller action and a Vue component.

The routes.

// Laravel
Route::get('/', 'WelcomeController@index')->name('welcome');
// Vue (This should be done in your app.js file)
let Home = require('./components/Home')
const routes = [
{ path: '/', component: Home }
]

The controller (Found in app/Http/Controllers/WelcomeController.php)

The welcome page request is handled by our WelcomeController which has only one method for now. The method will return our welcome view.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class WelcomeController extends Controller
{

public function index()
{
return view('welcome');
}
}

The component (Found in resources/js/components/Home.vue)

<template>
<div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="jumbotron text-center">
<h2>Welcome to Larafirebase</h2>
<p>
This is a simple laravel blog app with Google firebase. Choose your preference below to continue.
</p>
<p class="mt-5">
<router-link to="/post/add" class="btn btn-primary mr-3"><i class="sl sl-icon-plus"></i> New Post</router-link>
<router-link to="/posts" class="btn btn-secondary"><i class="sl sl-icon-notebook"></i> All Posts</router-link>
</p>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: "Home"
}
</script>

<style scoped>

</style>
What our welcome page looks like.

2. Rendering our add post page

The add post page, has Laravel and Vue routes. We then have the AddPost.vue component.

The routes

// Laravel
Route::post('/post/store', 'PostsController@store')->name('posts.store');
// Vue
{ path: '/post/add', component: AddPost }

The controller action

This snippet is from our app/Http/Controllers/PostsController.php

public function store(Request $request)
{
$this->checkInternetConnection();

$insertRecord = $this->pfb->insertRecord([
'title' => $request->title,
'body' => $request->body,
'date' => now()->toDateTimeString()
], true);

return response()->json([
'connected' => true,
'post' => $insertRecord
]);
}

The component (AddPost.vue)

This is found in resources/js/components/AddPost.vue

<template>
<div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<span class="card-title">Add post</span>
<span class="float-right">
<router-link to="/posts" class=""><i class="sl sl-icon-notebook"></i> All Posts</router-link>
</span>
</div>
<div class="card-body">
<div class="" v-if="!isLoading">
<div class="form-group row">
<label for="title" class="col-md-2 col-form-label text-md-right">Post title</label>
<div class="col-md-10">
<input id="title" type="text" class="form-control" v-model="post.title" required>
</div>
</div>
<div class="form-group row">
<label for="body" class="col-md-2 col-form-label text-md-right">Body</label>
<div class="col-md-10">
<textarea name="" id="body" cols="3" rows="10" class="form-control" v-model="post.body"></textarea>
</div>
</div>
<div class="form-group row">
<label for="title" class="col-md-2 col-form-label text-md-right"></label>
<div class="col-md-10">
<button class="btn btn-primary mr-2" @click.prevent="saveRecords()"><i class="sl sl-icon-cloud-upload"></i> Save Post</button>
<button class="btn" @click.prevent="closeAdd()"><i class="sl sl-icon-close"></i> Cancel</button>
</div>
</div>
</div>
<div class="row justify-content-center" v-if="!isConnected">
<div class="col-md-8 m-3">
<div class="alert alert-danger">
<i class="sl sl-icon-info"></i> You are not connected to active internet!
</div>
</div>
</div>
<div v-if="isLoading" class="row justify-content-center">
<div class="col-md-2 pt-3 pb-3 text-center">
<img :src="spinner" alt="" class="spinner">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: "AddPost",
data() {
return {
post: {
title: '',
body: ''
},
spinner: loadingSpinner,
isLoading: false,
isConnected: true
}
},

methods: {
async saveRecords() {
this.isLoading = true;
if (this.post.title == '') {
alert('Enter post title');
this.isLoading = false;

return false;
}

if (this.post.body == '') {
alert('Enter post body');
this.isLoading = false;

return false;
}

await axios.post(`${baseUrl}/post/store`, {
title: this.post.title,
body: this.post.body
}).then(response => {
this.isLoading = false;
if (response.data.connected) {
this.isConnected = true;
this.$router.push({
path: `/post/${response.data.post.id}/edit/`
})
} else {
this.isConnected = false;
}
}).catch(error => {
this.isLoading = false;
alert('Failed to create post');
console.log(error.message)
})
},

closeAdd() {
this.$router.push('/')
}
}
}
</script>

<style scoped>
.spinner {
width: 50px;
}
</style>
How our Add post page looks like.

3. Rendering our Posts page

This page displays all our stored posts on Google Firebase. Just like the steps taken to display our add page, we need controller, routes and components to render our Posts page. However, we will be doing a small http request to our server to get all stored posts. Let’s see how we can do all these.

The routes

// Laravel
Route::post('/posts', 'PostsController@index')->name('posts.index');
// Vue
{ path: '/posts', component: Posts }

The controller (found in app/Http/Controllers/PostsController.php)

public function index()
{
$this->checkInternetConnection();

return response()->json([
'connected' => true,
'posts' => $this->pfb->getRecords()
]);
}

The component (found in resources/js/components/Posts.vue)

<template>
<div class="row justify-content-center">
<div class="col-md-9">
<div class="card">
<div class="card-header">
<span class="card-title">All posts</span>
<span class="float-right">
<router-link to="/post/add" class="btn btn-sm btn-primary"><i class="sl sl-icon-plus"></i> New Post</router-link>
</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table" v-if="!isLoading && isConnected">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(post, key) in posts" v-if="post != null">
<td>{{ post.id }}</td>
<td>{{ post.title }}</td>
<td>
<div class="btn-group" role="group" aria-label="Action buttons">
<router-link :to="`/post/${post.id}/read`" class="btn btn-success btn-sm" title="Read Post" data-toggle="tooltip">
<i class="sl sl-icon-eye"></i>
</router-link>
<router-link :to="`/post/${post.id}/edit`" class="btn btn-primary btn-sm" title="Edit Post" data-toggle="tooltip">
<i class="sl sl-icon-note"></i>
</router-link>
<a href="" @click.prevent="deletePost(key, post.id)" class="btn btn-danger btn-sm" title="Delete Post">
<i class="sl sl-icon-trash"></i>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="row justify-content-center" v-if="!isLoading && !isConnected">
<div class="col-md-6 m-3">
<NoInternet></NoInternet>
</div>
</div>
<div v-if="isLoading" class="row justify-content-center">
<div class="col-md-2 pt-4 pb-4 text-center">
<img :src="spinner" alt="" class="spinner">
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
let NoInternet = require('../components/NoInternet')
export default {
name: "Posts",
components: {
NoInternet
},
data() {
return {
posts: {},
spinner: loadingSpinner,
isLoading: true,
isConnected: false
}
},
mounted() {
this.getAllPosts();
},
methods: {
async getAllPosts() {
await axios.post(`${baseUrl}/posts`, {}).then(response => {
this.isLoading = false;
if (response.data.connected) {
this.isConnected = true;
this.posts = response.data.posts;
}
console.log(response);
}).catch(error => {
this.isLoading = false;
console.log(error.message)
});
},

async deletePost(key, postId) {
if (confirm("Are you sure you want to delete this post?")) {
this.isLoading = true;
this.posts.splice(key, 1);
await axios.post(`${baseUrl}/post/delete`, {postId: postId})
.then(response => {
this.isLoading = false;
if (!response.data.connected) {
alert('Connect to internet please.')
}
}).catch(error => {
this.isLoading = false;
console.log(error.message)
})
}
}
}
}
</script>

<style scoped>
.spinner {
width: 50px;
}
</style>
Posts page

4. Rendering our Edit Post page

To render this page, we need the laravel api route and vue route, controller as well as a component. Here, we will be checking for internet connection. There, we have a child component for this page called NoInternet.

The routes

// Laravel
Route::post('/post/edit', 'PostsController@edit')->name('post.edit');
// Vue
{ path: '/post/:postId/edit', component: EditPost }

Note: We have something like :postId. This, means that we are accepting the post id as a parameter from the url. So, we will have something like …./post/1/edit. The checkInternetConnection(), is a tiny method within our controller that checks for internet connection since we are communicating Firebase.

The controller action

This is done in app/Http/Controllers/PostsController.php

public function edit(Request $request)
{
$this->checkInternetConnection();
return response()->json([
'connected' => true,
'post' => $this->pfb->getRecord($request->postId)
]);
}

The component (EditPost.vue)

This component has a child component. All of which can be found inside resources/js/components. I will past the code for NoInternet component at the end of this article.

<template>
<div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<span class="card-title">Edit post</span>
<span class="float-right">
<router-link to="/post/add" class=""><i class="sl sl-icon-plus"></i> New Post</router-link>
</span>
</div>
<div class="card-body">
<div class="" v-if="!isLoading && isConnected">
<div class="form-group row">
<label for="title" class="col-md-2 col-form-label text-md-right">Post title</label>
<div class="col-md-10">
<input id="title" type="text" class="form-control" v-model="post.title" required>
</div>
</div>
<div class="form-group row">
<label for="body" class="col-md-2 col-form-label text-md-right">Body</label>
<div class="col-md-10">
<textarea name="" id="body" cols="3" rows="10" class="form-control" v-model="post.body"></textarea>
</div>
</div>
<div class="form-group row">
<label for="title" class="col-md-2 col-form-label text-md-right"></label>
<div class="col-md-10">
<button class="btn btn-primary mr-2" @click.prevent="saveAndStay()"><i class="sl sl-icon-cloud-upload"></i> Save changes</button>
<button class="btn" @click.prevent="closeEdit()"><i class="sl sl-icon-arrow-left-circle"></i> Back to posts</button>
</div>
</div>
</div>
<div class="row justify-content-center" v-if="!isLoading && !isConnected">
<div class="col-md-8 m-3">
<NoInternet></NoInternet>
</div>
</div>
<div v-if="isLoading" class="row justify-content-center">
<div class="col-md-2 pt-3 pb-3 text-center">
<img :src="spinner" alt="" class="spinner">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
let NoInternet = require('../components/NoInternet')
export default {
name: "EditPost",
components: {
NoInternet
},
data() {
return {
post: {},
isLoading: true,
isConnected: false,
spinner: loadingSpinner,
postId: this.$route.params.postId
}
},
mounted() {
this.getPost(this.postId);
},
methods: {
async getPost(id) {
await axios.post(`${baseUrl}/post/edit`, {postId: id})
.then(response => {
this.isLoading = false;
if (response.data.connected) {
this.isConnected = true;
this.post = response.data.post;
}
}).catch(error => {
this.isLoading = false;
console.log(error)
});
},

async saveAndStay() {
this.isLoading = true;
await axios.post(`${baseUrl}/post/update`, {
id: this.post.id,
title: this.post.title,
body: this.post.body
})
.then(response => {
this.isLoading = false;
alert("Post saved successfully");
}).catch(error => {
this.isLoading = false;
alert("Failed to update post!");
console.log(error.message)
})
},

closeEdit() {
this.$router.push('/posts');
}
}
}
</script>

<style scoped>
.spinner {
width: 50px;
}
</style>
What our Edit Post page looks like

5. Rendering Read Posts page

The read post page is where we display a single post to be read. This process works as the edit post. See how can achieve the read post page below.

The routes (Found in routes/web.php and resources/js/app.js)

// Laravel
Route::post('/post/show', 'PostsController@show')->name('post.show')
// Vue
{ path: '/post/:postId/read/', component: ReadPost }

The controller action (Found in app/Http/Controllers/PostsController.php)

public function show(Request $request)
{
$this->checkInternetConnection();

return response()->json([
'connected' => true,
'post' => $this->pfb->getRecord($request->postId)
]);
}

The component (Found in resources/js/components/ReadPost.vue)

<template>
<div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="row">
<div class="col-md-12 text-right mb-4">
<router-link to="/posts" class="btn btn-success" title="All posts"><i class="sl sl-icon-notebook"></i></router-link>
<router-link to="/post/add/" class="btn btn-primary" title="Add new post"><i class="sl sl-icon-plus"></i></router-link>
<router-link :to="`/post/${postId}/edit`" class="btn btn-secondary" title="Edit post"> <i class="sl sl-icon-note"></i></router-link>
<a href="" @click.prevent="deletePost(postId)" class="btn btn-danger" title="Delete post"> <i class="sl sl-icon-trash"></i></a>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title font-weight-bold" v-if="!isLoading && isConnected">{{ post.title }}</span>
<span class="card-title" v-if="isLoading || !isLoading && !isConnected">Read post</span>
</div>
<div class="card-body">
<div v-if="!isLoading && isConnected">
{{ post.body }}
<br/><br/>
<div class="divider"></div>
<span class="text-right text-muted">
<i class="sl sl-icon-calender"></i> {{ post.date }}
</span>
</div>
<div class="row justify-content-center" v-if="!isLoading && !isConnected">
<div class="col-md-7 m-3">
<NoInternet></NoInternet>
</div>
</div>
<div class="row justify-content-center" v-if="isLoading">
<div class="col-md-2 text-center pt-3 pb-3">
<img :src="spinner" alt="" class="spinner">
</div>
</div>
</div>
</div>

</div>
</div>
</div>
</template>

<script>
let NoInternet = require('../components/NoInternet')
export default {
name: "ReadPost",
components: {
NoInternet
},
data() {
return {
post: {},
postId: this.$route.params.postId,
isLoading: true,
spinner: loadingSpinner,
isConnected: false
}
},
mounted() {
axios.post(`${baseUrl}/post/show`, {postId: this.$route.params.postId})
.then(response => {
this.isLoading = false;
if (response.data.connected) {
this.isConnected = true;
this.post = response.data.post;
}
})
.catch(error => {
console.log(error.data.message)
})
},
methods: {
async deletePost(postId) {
this.isLoading = true;
if (confirm("Are you sure you want to delete this post?")) {
this.isLoading = true;
await axios.post(`${baseUrl}/post/delete`, {postId: postId})
.then(response => {
if (!response.data.connected) {
alert('Connect to internet please.')
}
this.$router.push('/posts')
}).catch(error => {
console.log(error.data.message)
})
} else {
this.isLoading = false;
}
}
}
}
</script>

<style scoped>
.spinner {
width: 50px;
}
.divider {
width: 100%;
display: block;
height: 2px;
margin-bottom: 20px;
}
</style>
The read post page

Summary

I earlier said I will be dropping the snippet for our NoInternet component. Here it is:

<template>
<div class="alert alert-danger">
<i class="sl sl-icon-info"></i> <b>Whoops</b>. Unable to connect to firebase!
<a href="#" class="btn btn-primary btn-sm" onclick="window.location.reload()"><b>Retry</b></a>
</div>
</template>

<script>
export default {
name: "NoInternet"
}
</script>

<style scoped>

</style>

This article, is not that explained but I do hope it would guide you to building your first app using PHP(Laravel) with Google Firebase.

I’ve hosted the app and you can as well find the source code on Github .

In case, you have any difficulty going through this tutorial or see need to contribute to the PHP library (), kindly find me on Twitter or on Github .

A big thank you to the creator of . You made it easier for the library I built.

Josiah Ovye Yahaya

Written by

Full Stack Developer. PHP, JavaScript, Python.