Огромная шумиха была вызвана Google Firebase. Однако PHP не имеет большой поддержки на платформе Google Firebase. Сегодня я расскажу вам, как легко можно создать приложение CRUD с помощью PHP и Firebase.

Эта статья будет о простом приложении, которое я недавно создал с помощью PHP (Laravel) и Google Firebase. Я расскажу вам шаг за шагом, как мне это сделать.

Что нам нужно

Для каждого проекта важны планирование и сроки. То же самое и с этим маленьким. Ниже перечислены вещи, которые нам нужны, чтобы достичь того, чего мы хотим, и как быстро мы хотим это сделать.

Инструменты

  1. Библиотека PHPFirebase
  2. Laravel
  3. VueJS
  4. Любой хороший редактор или IDE (я использую PHPStorm)

Время

~ 1 час 30 мин.

Начиная

Я начал с установки библиотеки Laravel и PHPFirebase для выполнения действий CRUD в Google Firebase.

Поскольку у меня уже есть глобально установленный laravel на моей машине, я просто сделал это.

laravel new larafirebase

Далее идет установка библиотеки.

composer require coderatio/phpfirebase:v1.0

Поскольку я буду использовать VueJS в качестве интерфейса, мне пришлось установить все зависимости npm. Я сделал это с помощью этой команды.

npm install

Инициализация нашего приложения с помощью VueJS

Мы сделаем это в нашем файле app.js внутри папки resources = ›js.

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
});

Создание наших контроллеров Laravel

Для этого проекта нам понадобятся всего два контроллера.

  1. WelcomeController (отвечает за обработку запроса страницы приветствия)
  2. PostController (отвечает за обработку всех наших запросов CRUD действий)

Создание наших компонентов Vue

Мы создадим компоненты для каждого действия CRUD, кроме компонента «Удалить» и компонента обзора.

  1. CreatePost или AddPost
  2. ReadPost
  3. UpdatePost или EditPost
  4. Сообщения (который отвечает за отображение всех наших созданных сообщений).

1. Отображение нашей страницы приветствия.

Чтобы отобразить нашу страницу приветствия, нам нужны маршруты (Laravel и Vue), действие контроллера и компонент Vue.

Маршруты.

// 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 }
]

Контроллер (находится в app / Http / Controllers / WelcomeController.php)

Запрос страницы приветствия обрабатывается нашим WelcomeController, у которого на данный момент есть только один метод. Метод вернет наше приветствие представление.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class WelcomeController extends Controller
{

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

Компонент (находится в 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>

2. Визуализация нашей страницы добавления сообщения.

На странице добавления сообщения есть маршруты Laravel и Vue. Затем у нас есть компонент AddPost.vue.

Маршруты

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

Действие контроллера

Этот фрагмент взят из нашего 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
    ]);
}

Компонент (AddPost.vue)

Он находится в 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>

3. Отображение страницы с публикациями.

На этой странице отображаются все наши сохраненные сообщения в Google Firebase. Так же, как шаги, предпринятые для отображения нашей страницы добавления, нам нужны контроллер, маршруты и компоненты для отображения нашей страницы Сообщения. Однако мы сделаем небольшой HTTP-запрос к нашему серверу, чтобы получить все сохраненные сообщения. Посмотрим, как мы можем все это сделать.

Маршруты

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

Контроллер (находится в app / Http / Controllers / PostsController.php)

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

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

Компонент (находится в 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>

4. Отображение нашей страницы редактирования сообщения

Чтобы отобразить эту страницу, нам понадобятся маршрут api laravel и маршрут vue, контроллер, а также компонент. Здесь мы будем проверять подключение к Интернету. У нас есть дочерний компонент для этой страницы под названием NoInternet.

Маршруты

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

Примечание. У нас есть что-то вроде postId. Это означает, что мы принимаем идентификатор сообщения в качестве параметра URL-адреса. Итак, у нас будет что-то вроде …. / Post / 1 / edit. checkInternetConnection (), - это крошечный метод в нашем контроллере, который проверяет подключение к Интернету, поскольку мы обмениваемся данными с Firebase.

Действие контроллера

Это делается в app / Http / Controllers / PostsController.php.

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

Компонент (EditPost.vue)

У этого компонента есть дочерний компонент. Все это можно найти внутри resources / js / components. Я пропущу код компонента NoInternet в конце этой статьи.

<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>

5. Отображение страницы "Прочитанные сообщения"

На странице прочитанного сообщения мы отображаем одно сообщение, которое нужно прочитать. Этот процесс работает как пост редактирования. Посмотрите, как можно достичь страницы чтения сообщения ниже.

Маршруты (находятся в routes / web.php и resources / js / app.js)

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

Действие контроллера (находится в app / Http / Controllers / PostsController.php)

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

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

Компонент (находится в 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>

Резюме

Ранее я сказал, что опущу фрагмент для нашего компонента NoInternet. Вот он:

<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>

В этой статье нет подробных объяснений, но я надеюсь, что она поможет вам создать свое первое приложение с использованием PHP (Laravel) с Google Firebase.

Я разместил приложение Здесь, и вы также можете найти исходный код на Github Здесь.

В случае, если у вас возникнут какие-либо трудности при прохождении этого руководства или вы видите необходимость внести свой вклад в библиотеку PHP (PHPFirebase), любезно найдите меня в Twitter Здесь или на Github Здесь.

Большое спасибо создателю Firebase Admin SDK для PHP. Вы упростили работу с созданной мной библиотекой.