Tampilan message dari Laravel Realtime Chat Pusher
Tampilan message dari Laravel Realtime Chat Pusher

Membuat Real Time Chatting Laravel dan Pusher

profil
Febri Hidayan
22 August 2020 ยท Baca 36 menit

Hai semuanya, kesempatan kali ini saya membagikan sedikit pengetahuan saya. Bagaimana sih cara membuat real time chatting dan bagaimana mengaplikasikan di Laravel. Tentu hal itu menjadi masalah tersendiri, masalah saya ketika membuat real time chatting itu dibagian struktur tabel Messages apa saja field yang harus di buat sampai-sampai sehari suntuk mencari informasi lebih lanjut untuk itu. Sekarang waktunya saya untuk membagikannya untuk Anda semua, barangkali ini suatu hal yang bermanfaat. Simak penjelasannya berikut ini.

Daftar isi:

Baca juga: Membuat Live Search Dengan Laravel dan Vue.js

Langkah 1 - Installasi

Hal yang perlu di persiapkan untuk mengikuti tutorial ini dengan menginstal beberapa paket composer dan npm.

Composer:

composer require laravel/ui pusher/pusher-php-server

Npm:

npm install laravel-echo pusher-js vue vue-template-compiler

atau gunakan yarn

yarn add laravel-echo pusher-js vue vue-template-compiler

Setelah itu jangan lupa jalankan perintah composer berikut untuk menginstal komponen bootstrap laravel ui.

php artisan ui bootstrap --auth

Langkah 2 - Konfirgurasi dan Table Messages

Saya harap Anda sudah membuat akun pusher dan menambahkan aplikasi di pusher untuk mendapatkan app_id, key, dan secret. Sebelum itu Anda sudah melakukan konfirgurasi buat basis datanya.

Sesuaikan kode dibawah ini pada berkas .env aplikasi Anda.

PUSHER_APP_ID=sesuaikan
PUSHER_APP_KEY=sesuaikan
PUSHER_APP_SECRET=sesuaikan
PUSHER_APP_CLUSTER=ap1

Makna sesuaikan: isikan berdasarkan app keys yang ada pada dashboard pusher.

Buat tabel baru dengan nama Messages, jalankan perintah artisan berikut untuk menambahkan berkas di database\migrations.

php artisan make:migration CreateMessagesTable

Kemudian tambahakan skrip di bawah ini untuk menambahkan berkas tabel.

...
$table->unsignedBigInteger('from_id');
$table->unsignedBigInteger('to_id');
$table->text('content');
$table->dateTime('read_at')->nullable();
...

Jalankan perintah artisan berikut untuk migrate semua tabel. Maka, setelah ini semua tabel berhasil di tambahkan ke basis data yang sudah ditentukan.

php artisan migrate

Langkah 3 - Konfirgurasi berkas app.js, bootstrap.js, dan app.scss

Hilangkan komentar skrip seperti dibawah ini pada berkas bootstrap.js.

import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: true
});

Kemudian tambahkan baris baru pada berkas app.js. Kita akan banyak bermain di app.js.

import Vue from 'vue'
new Vue({
    el: '#app'
})

dan begitu juga di berkas app.scss

.list-user, .card-message {
    overflow-y: auto;
    height: 400px;
}
.list-user .list-group-item {
    border: none;
    &.active {
        color: #495057;
        background-color: #f7f7f7;
    }
}
.card-message {
    .list-group-item {
        padding: 0;
        border: none;
    }
    .list-message-item {
        width: 80%;
        
        @media screen and (min-width: 768px) {
            width: 49%;
        }
    
        &.right {
            float: right;
        }
    }
}
.rounded-circle {
    width: 48px;
}
.svg-icon {
  width: 1.25rem;
  height: 1.25rem;
}
.svg-icon path{
  fill: #4691f6;
}
[badge]:after {
    background: hsl(171, 100%, 41%);
    border-radius: 10px;
    color: #fff;
    content: attr(badge);
    font-size: 11px;
    margin-top: -5px;
    margin-left: -8px;
    min-width: 20px;
    padding: 2px;
    position: absolute;
    text-align: center;
}
[badge^="-"]:after,
[badge="0"]:after,
[badge=""]:after {
   display: none;
}

Langkah 4 - Bekerja di Frontend

Pada bagian ini sangat banyak skrip yang di bahas maka dari itu perhatikan setiap langkahnya, agar tidak tersesat di jalan ๐Ÿ˜.

Baca juga: Tutorial PHP Untuk Pemula

Tambahkan baris baru pada berkas layouts\app.blade.php di bawah csrf token tag head. Hal ini dilakukan untuk mengambil id user ketika sudah login, nanti kita bahas lebih lanjut di berkas app.js.

@auth
<meta name="user_id" content="{{ auth()->user()->id }}">
@endauth

dan juga tambahkan skrip di bawah ini untuk notifikasinya. Pada Authentication Links di navbar tepatnya di bawah @else.

<li class="nav-item">
    <a href="{{ route('home') }}" class="nav-link" :badge="notif">
        <svg class="svg-icon" viewBox="0 0 20 20">
            <path d="M14.38,3.467l0.232-0.633c0.086-0.226-0.031-0.477-0.264-0.559c-0.229-0.081-0.48,0.033-0.562,0.262l-0.234,0.631C10.695,2.38,7.648,3.89,6.616,6.689l-1.447,3.93l-2.664,1.227c-0.354,0.166-0.337,0.672,0.035,0.805l4.811,1.729c-0.19,1.119,0.445,2.25,1.561,2.65c1.119,0.402,2.341-0.059,2.923-1.039l4.811,1.73c0,0.002,0.002,0.002,0.002,0.002c0.23,0.082,0.484-0.033,0.568-0.262c0.049-0.129,0.029-0.266-0.041-0.377l-1.219-2.586l1.447-3.932C18.435,7.768,17.085,4.676,14.38,3.467 M9.215,16.211c-0.658-0.234-1.054-0.869-1.014-1.523l2.784,0.998C10.588,16.215,9.871,16.447,9.215,16.211 M16.573,10.27l-1.51,4.1c-0.041,0.107-0.037,0.227,0.012,0.33l0.871,1.844l-4.184-1.506l-3.734-1.342l-4.185-1.504l1.864-0.857c0.104-0.049,0.188-0.139,0.229-0.248l1.51-4.098c0.916-2.487,3.708-3.773,6.222-2.868C16.187,5.024,17.489,7.783,16.573,10.27"></path>
        </svg>
    </a>
</li>

Sedikit penjelasan: jika ada user yang mengirim Anda pesan yang tidak dilihat maka notifikasi akan memberikan jumlah user yang Anda. Harus di ketahui jumlah nya itu bukan pesan yang sudah di lihat atau dibaca.

Lebih dalam menggunakan Vue.js di berkas app.js.

new Vue({
    el: '#app',
    data: {
        // #data
    },
    mounted() {
        // #mounted
    },
    methods: {
        // #methods
    },
    watch: {
        // #watch
    }
})

#data: Pada bagian ini Anda bisa membuat variable secara global seperti di PHP CLASS. Variable tersebut akan di panggil dengan perintah this contoh variable nama menjadi this.nama.

#mounted: Ini akan di panggil setelah instance dipasang, kalau di PHP seperti __counstruct(). Apabila ingin memanggil variable atau func yang akan dijalankan pertama kali maka mounted pilihan yang tepat.

#methods: Secara umum digunakan untuk membuat function.

#watch: Akan di jalankan setiap ada perubahan data, apa maksudnya? Jadi apabila Anda memiliki variable search setiap kali ketik akan memberikan perubahan data. Ingat!, menggunakannya dengan function dan waktu reload-Nya bisa ditentukan.

Baca juga: Tutorial CRUD Laravel dan Vue.js

Tambahkan skrip #data

id: document.querySelector('meta[name="user_id"]').content,
search: '',
messages: [],
users: [],
form: {
    to_id: '',
    content: ''
},
isActive: null,
notif: 0

#id untuk mengambil id user lihat di berkas layouts\app.blade.php.

#search untuk mencari nama pengguna.

#messages dan #users untuk menempatkan semua data.

#form untuk form pesan pengguna.

#isActive untuk memberikan hover kepada pengguna yang di lihat.

#notif untuk menghitung jumlah notifikasi lihat di berkas layouts\app.blade.php.

Tambahkan skrip #methods

// mengeluarkan semua pengguna
fetchUsers() {
    let q = _.isEmpty(this.search) ? 'all' : this.search
    
    axios.get('/message/user/' + q).then(({ data }) => {
        this.users = data
    })
},
// mengeluarkan semua messages dari user yang dipilih
fetchMessages(id) {
    this.form.to_id = id
    axios.get('/message/user-message/' + id).then(({ data }) => {
        this.messages = data
        this.isActive = this.users.findIndex((s) => s.id === id)
        this.users[this.isActive].count = 0
        this.notif--
    })
},
// mengirim pesan yang dikirim
sendMessage() {
    axios.post('message/user-message', this.form).then(({ data }) => {
        this.pushMessage(data, data.to_id)
        this.form.content = ''
        this.search = ''
    })
},
// fungsi untuk push laravel echo dan pusher
fetchPusher() {
    Echo.channel('user-message.' + this.id)
        .listen('MessageEvent', (e) => {
            this.pushMessage(e, e.from_id, 'push')
        })
},
// semua akan di eksekusi disini
pushMessage(data, user_id, action = '') {
    let index = this.users.findIndex((s) => s.id === user_id)
    if (index != -1 && action == 'push') {
        this.users.splice(index, 1) // menghapus user
    }
    /**
     * if untuk pesan submit
     */
    if (action == '') {
        this.users[index].content = data.content
        this.users[index].to_id = data.to_id
        let user = this.users[index]
        this.users.splice(index, 1)
        this.users.unshift(user)
    }
    /**
     * else untuk pesan dari laravel echo
     */
    else {
        this.users.unshift(data) // menambahkan user baru ke atas
    }
    /**
     * Jika dia melihat pesan user yang dipilih
     */
    if (this.form.to_id != '') {
        index = this.users.findIndex((s) => s.id === this.form.to_id)
        this.users[index].count = 0
        this.isActive = index
        if (this.form.to_id == user_id) {
            this.messages.push({
                avatar: data.avatar,
                content: data.content,
                created_at: data.created_at,
                from_id: data.from_id,
            })
            axios.get('/message/user-message/' + user_id + '/read')
        }
    }
},
// agar scroll ke arah pesan yang baru
scrollToEnd: function () {
    let container = this.$el.querySelector("#card-message-scroll");
    container.scrollTop = container.scrollHeight;
}

Tambahkan skrip #mounted

this.fetchUsers() // memanggil semua user yang di chat
this.fetchPusher() // untuk menjalankan laravel echo dan pusher

Tambahkan skrip #watch

// untuk mencari user
search: _.debounce( function() {
    this.fetchUsers()
}, 500),
// untuk menambahakan jumlah notifikasi berdasarkan perubahan variable users
users: _.debounce( function() {
    this.notif = 0
    this.users.filter(e => {
        if (e.count) {
            this.notif++
        }
    })
}),
// setiap ada pesan baru akan memanggil this.scrollToEnd()
messages: _.debounce( function() {
    this.scrollToEnd()
}, 10)

Bagian ini sudah selesai tinggal mengikuti langkah selanjutnya.

Baca juga: Aplikasi Blog Sederhana Dengan Laravel

Lebih dalam di berkas home.blade.php

Tidak ada salahnya untuk menggunakan berkas tersebut untuk meletakan messages, sebagai awal pengembangan. Ganti section content dengan skrip di bawah ini. Selengkapnya lihat di Github.

@section('content')
<div class="container">
    <div class="card">
        <div class="card-header text-center">Messages</div>
        <div class="card-body">
            <div class="row">
                <div class="col-md-4">
                    <div class="form-group">
                        <input v-model="search" type="search" class="form-control" placeholder="Cari user">
                    </div>
                    <ul class="list-user list-group list-group-flush">
                        <a v-for="(user, index) in users" v-bind:key="index"
                            v-if="user.id != id"
                            :class="['list-group-item d-flex justify-content-between align-items-center list-group-item-action', {
                                'active': isActive === index && search == '' ? true : false
                            }]"
                            v-on:click="fetchMessages(user.id)">
                            <div class="media">
                                <img class="mr-3 rounded-sm rounded-circle" :src="user.avatar" alt="profile">
                                <div class="media-body">
                                    <strong>@{{ user.name }}</strong>
                                    <p v-if="user.content">
                                        @{{
                                            (id != user.to_id ? 'Anda: ' : '')
                                            + (user.content.length > 20 
                                            ? user.content.substr(0, 20) + '...' 
                                            : user.content)
                                        }}
                                    </p>
                                </div>
                            </div>
                            <span v-if="user.count" class="badge badge-primary badge-pill mr-3">@{{ user.count }}</span>
                        </a>
                    </ul>
                </div>
                <div class="col-md-8">
                    <div class="card">
                        <div class="card-body card-message" id="card-message-scroll">
                            <ul v-if="isActive != null" class="list-group list-group-flush">
                                <div v-for="(message, index) in messages" v-bind:key="index">
                                    <li v-if="message.from_id != {{ auth()->user()->id }}" class="list-group-item">
                                        <div class="list-message-item">
                                            <div class="media">
                                                <img class="mr-3 rounded-sm rounded-circle" :src="message.avatar" alt="profile">
                                                <div class="media-body">
                                                    <div class="alert alert-primary mb-0">
                                                        @{{ message.content }}
                                                    </div>
                                                    <small><i>@{{ new Date(message.created_at).toLocaleDateString()}}</i></small>
                                                </div>
                                            </div>
                                        </div>
                                    </li>
                                    <li v-else class="list-group-item">
                                        <div class="list-message-item right">
                                            <div class="alert alert-secondary mb-0">
                                                @{{ message.content }}
                                            </div>
                                            <small class="float-right"><i>@{{ new Date(message.created_at).toLocaleDateString()}}</i></small>
                                        </div>
                                    </li>
                                </div>
                            </ul>
                            <h5 v-else class="text-center">Pilih user untuk mengirim pesan</h5>
                        </div>
                    </div>
                    <div v-if="isActive != null" class="form-group mt-3">
                        <form @submit.prevent="sendMessage">
                            <input v-model="form.content" type="text" class="form-control" placeholder="Tulis..." required>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Penjelasan singkat: col-md-4 untuk tampilan pengguna dan col-md-8 untuk tampilan messages.

Langkah 5 - Membuat Routing

Tambahkan baris baru untuk melatakan rute ini, cukup sedikit yang perlu ditambahkan.

Route::group(['prefix' => 'message'], function () {
    Route::get('user/{query}', 'MessageController@user');
    Route::get('user-message/{id}', 'MessageController@message');
    Route::get('user-message/{id}/read', 'MessageController@read');
    Route::post('user-message', 'MessageController@send');
});

Langkah 6 - Model dan Controller

Pada langkah ini membuat model dan controller, tapi jangan terburu buru masih ada berkas yang belum di buat seperti events dan resources.

Tambahkan skrip model User.php

Tambahkan baris baru di model User.php, seperti di bawah ini. Lihat di Github User.php.

protected $appends = [
    'avatar'
];
public function getAvatarAttribute()
{
    return 'https://www.gravatar.com/avatar/' . md5(strtolower($this->email));
}
public function messagesTo()
{
    return $this->hasOne(Message::class, 'to_id')->latest();
}
public function messagesFrom()
{
    return $this->hasOne(Message::class, 'from_id')->latest();
}

Membuat Model dan Controller Message

Jalankan perintah artisan di bawah ini untuk membuat model dan controller sekaligus.

php artisan make:model Message -c

Tambahkan baris untuk skrip di bawah ini di model Message.php.

protected $fillable = [
    'from_id', 'to_id', 'content', 'read_at'
];
protected $casts = [
    'read_at' => 'datetime',
];
public function users()
{
    return $this->belongsTo(User::class, 'from_id');
}
public function userFrom()
{
    return $this->belongsTo(User::class, 'from_id');
}
public function userTo()
{
    return $this->belongsTo(User::class, 'to_id');
}

Saya sengaja melakukan function users() dan userFrom() sama, agar lebih mudah memahaminya. Anda bisa menghapus salah satunya dan mengantinya di bagian berkas MessageController.php.

Tambahkan baris untuk skrip di bawah ini di controller MessageController.php. Lihat di Github MessageController.php.

// Letakan ini di bawah use, ingat use bawaan tidak ada disini
use App\Events\MessageEvent;
use App\Http\Resources\MessageResource;
use App\Http\Resources\UserResource;
use App\Message;
use App\User;
use Illuminate\Support\Facades\DB;

// Letakan ini di dalam Class MessageController
protected function user($query)
{
    $field = ['id', 'name', 'email'];
    $id = auth()->user()->id;
    if ($query === 'all') {
        $users = Message::with(['userFrom', 'userTo'])
            ->where('messages.to_id', $id)
            ->orWhere('messages.from_id', $id)
            ->latest();
        $keys = [];
        foreach ($users->get() as $key => $user) {
            if ($user->userFrom->id == $id) {
                $keys[$key] = $user->userTo->id;
            } else {
                $keys[$key] = $user->userFrom->id;
            }
        }
        $keys = array_unique($keys);
        $ids = implode(',', $keys);
        $users = User::whereIn('id', $keys);
        if (!empty($key)) {
            $users = $users->orderByRaw(DB::raw("FIELD(id, $ids)"));
        }
    } else {
        $users = User::where('name', 'like', "%{$query}%")->where('id', '!=', $id);
    }
    $users = UserResource::collection($users->get($field));
    return response()->json($users);
}
protected function message($id)
{
    $to = [
        ['from_id', $id],
        ['to_id', auth()->user()->id]
    ];
    $messages = Message::with('users')->where($to);
    $first = $messages;
    if ($first->exists()) {
        DB::table('messages')->where($to)->update(['read_at' => now()]);
    }
    $messages = $messages->orWhere([
        ['from_id', auth()->user()->id],
        ['to_id', $id]
    ])->get();
    $messages = MessageResource::collection($messages);
    return response()->json($messages);
}
protected function send(Request $request)
{
    $request->merge(['from_id' => auth()->user()->id]);
    $message = Message::create($request->all());
    event(new MessageEvent($message));
    $message = new MessageResource($message);
    return response()->json($message);
}
protected function read($id)
{
    $to = [
        ['from_id', $id],
        ['to_id', auth()->user()->id]
    ];
    DB::table('messages')->where($to)->update(['read_at' => now()]);
}

Langkah 7 - Events dan Resources

Agar lebih cepat membuat 3 berkas baru yaitu MessageEvent.php, MessageResource.php, dan UserResource.php.

php artisan make:event MessageEvent
php artisan make:resource MessageResource
php artisan make:resource UserResource

Tambahkan skrip untuk berkas MessageEvent.php.

<?php
namespace App\Events;

use App\Http\Resources\MessageResource;
use App\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    private $field;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($field)
    {
        $this->field = $field;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('user-message.' . $this->field->to_id);
    }

    /**
     * Get the data to broadcast.
     *
     * @return array
     */
    public function broadcastWith()
    {
        $message = json_decode((new MessageResource($this->field))->toJson(), true);
        return $message;
    }
}

Jadi begini cara kerjanya, pertama panggil kelas MessageEvent pada MessageController di function send() untuk mengambil satu pesan yang barus di insert. Kemudian MessageEvent meneruskannya dengan mengambil dengan __construct() yang di berikan pada variabel $field. Seterusnya kepada siapa pesan ini diterima, maka broadcastOn() akan dieksekui sesuai dengan id penerima yaitu to_id. Selanjutnya apa saja yang dikirim, maka broadcastWith() yang berisi resource yang sudah ditentukan.

Tambahkan skrip untuk berkas MessageResource.php.

Berisi semua data pesan yang sudah disesuaikan dengan tampilannya di frontend.

<?php

namespace App\Http\Resources;

use App\Message;
use Illuminate\Http\Resources\Json\JsonResource;

class MessageResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        $count = Message::where([
            ['from_id', $this->from_id],
            ['to_id', $this->to_id],
        ])->whereNull('read_at')->count();


        return [
            'id' => $this->users->id,
            'name' => $this->users->name,
            'avatar' => $this->users->avatar,
            'from_id' => $this->from_id,
            'to_id' => $this->to_id,
            'content' => $this->content,
            'created_at' => $this->created_at,
            'count' => $count
        ];
    }
}

Tambahkan skrip untuk berkas UserResource.php.

Mengeluarkan semua data pengguna yang dikirimnya.

<?php

namespace App\Http\Resources;

use App\Message;
use App\User;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        $to = [
            ['to_id', auth()->user()->id],
            ['from_id', $this->id]
        ];
        
        $message = Message::where([
            ['from_id', auth()->user()->id],
            ['to_id', $this->id]
        ])->orWhere($to)->latest()->first();


        $count = Message::where($to)->whereNull('read_at')->count();
        
        return [
            'id' => $this->id,
            'name' => $this->name,
            'avatar' => $this->avatar,
            'from_id' => $message->from_id ?? '',
            'to_id' => $message->to_id ?? '',
            'content' => $message->content ?? '',
            'count' => $count
        ];
    }
}

Silakan berkomentar jika dari 3 berkas ini tidak paham, agar saya tahu apa saja yang perlu dijelaskan dan mengapa melakukan ini.

Baca juga: Cara Kirim Email Di Laravel

Langkah 8 - Menjalankan dan Jalankan

Tahap terakhir ini adalah pengujian, apakah semua langkah sudah berjalan dengan lancar atau masih ada yang di lewatkan. Cukup jalankan 2 perintah untuk menjalankanya.

  1. php artisan serve
  2. npm run watch atau yarn watch

Jangan lupa untuk menginstal Node.js, jika sudah tapi masih galat saat menjalankan npm atau yarn. Pastikan Anda sudah menginstal semua paket dengan perintah npm install atau yarn. Kemudian ulangi lagi perintah nomor 2.

Terimakasih ๐Ÿ‘, jangan lupa like dan komentarnya.

Link github Laravel Realtime Chat Pusher.


profil

DITULIS OLEH

Febri Hidayan

Belajar dari sekarang untuk meningkatkan ilmu dan karirmu. Amati Tiru Modifikasi