menu

Engine - Push websocket from API Laravel Part-1


Pada tutorial sebelumnya kita sudah membuat sebuah engine push notifikasi dan chat menggunakan Ratchet Laravel yang hanya terjadi antara client, lantas bagaimana jika kita ingin mengirimkan pusher tersebut melalui API pada framework Laravel? tentu akan ada beberapa tambahan setup untuk itu.

Baiklah, dari case yang ada diatas pada tutorial kali ini kita akan membuat sebuah engine untuk mengirimkan notifikasi atau chat dari sebuah API menggunakan Ratchet Laravel.

Requirements yang codedoct gunakan adalah,
  1. Laravel versi 5.4
  2. PHP versi 5.6
  3. Ratchet cboden versi 0.4
  4. ZMQ versi 0.3
Sebelum memulai, pastikan ZeroMQ sudah terinstall pada PHP dan web server, jika belum klik disini(MAC OS).

Oke langsung saja kita mulai eksperimennya.
Pertama, install dulu requirement yang kita butuhkan, edit file composer.json dengan menambahkan code berikut,
"require": {
 "cboden/ratchet": "0.4.*",
 "react/zmq": "0.3.*"
}
dan update file composer,
$ composer update

Selanjutnya, buat file command laravel untuk dijalankan di server dengan cara,
$ php artisan make:command WebSocketServer --command=websocket:init

Syntax di atas akan secara otomatis membuat file baru dengan nama WebSocketServer.php pada path app/Console/Commands/ edit file tersebut sehingga akan tampak seperti ini,
<?php

namespace App\Console\Commands;

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Wamp\WampServer;

use React\EventLoop\Factory;
use React\ZMQ\Context;
use React\Socket\Server;

use Illuminate\Console\Command;
use App\Http\Controllers\Thirdparty\WebSocketPusherController;

class WebSocketServer extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'websocket:init';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->info("start server");
        $loop   = Factory::create();
        $pusher = new WebSocketPusherController;

        // Listen for the web server to make a ZeroMQ push after an ajax request
        $context = new Context($loop);
        $pull    = $context->getSocket(\ZMQ::SOCKET_PULL);

        // Binding to 127.0.0.1 means the only client that can connect is itself
        $pull->bind('tcp://127.0.0.1:5555');
        $pull->on('message', array($pusher, 'onBlogEntry'));

        // Binding to 0.0.0.0 means remotes can connect
        $webSock = new Server('0.0.0.0:8080',$loop);
        $webServer = new IoServer(
            new HttpServer(
                new WsServer(
                    new WampServer(
                        $pusher
                    )
                )
            ),
            $webSock
        );

        $loop->run();
    }
}

Pada code diatas kita membuat sebuah depedency dengan sebuah file WebSocketPusherController, buat file baru dengan nama WebSocketPusherController.php pada path app/Http/Controllers/Thirdparty/ dan isi dengan code berikut,
<?php

namespace App\Http\Controllers\Thirdparty;

use Ratchet\ConnectionInterface;
use Ratchet\Wamp\WampServerInterface;

class WebSocketPusherController implements WampServerInterface {
     /**
     * A lookup of all the topics clients have subscribed to
     */
    protected $subscribedTopics = array();

    public function onSubscribe(ConnectionInterface $conn, $topic) {
        $this->subscribedTopics[$topic->getId()] = $topic;
    }

    /**
     * @param string JSON'ified string we'll receive from ZeroMQ
     */
    public function onBlogEntry($entry) {
        $entryData = json_decode($entry, true);

        // If the lookup topic object isn't set there is no one to publish to
        if (!array_key_exists($entryData['type'], $this->subscribedTopics)) {
            return;
        }
        $topic = $this->subscribedTopics[$entryData['type']];

        // re-send the data to all the clients subscribed to that category
        $topic->broadcast($entryData);
    }

    /* The rest of our methods were as they were, omitted from docs to save space */
    public function onUnSubscribe(ConnectionInterface $conn, $topic) {
    }
    public function onOpen(ConnectionInterface $conn) {
    }
    public function onClose(ConnectionInterface $conn) {
    }
    public function onCall(ConnectionInterface $conn, $id, $topic, array $params) {
        // In this application if clients send data it's because the user hacked around in console
        $conn->callError($id, $topic, 'You are not allowed to make calls')->close();
    }
    public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) {
        // In this application if clients send data it's because the user hacked around in console
        $conn->close();
    }
    public function onError(ConnectionInterface $conn, \Exception $e) {
    }
}

Code websocket server diatas akan kita panggil melalui command php artisan untuk itu kita harus menambahkannya pada kernel laravel, yang terletak pada path app/Console/ dengan nama file Kernel.php edit bagian command menjadi seperti ini,
/**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        Commands\WebSocketServer::class,
    ];

Oke jika semua sudah selesai saatnya kita coba run WebSocketServer nya dengan cara,
$ php artisan websocket:init

Sehingga akan tampak seperti ini tampilannya,


===DONE!===

Engine - Install zeroMQ and binding PHP ZMQ (MAC OS)


Pada tutorial kali ini codedoct akan share pengalaman saat setup ZMQ di MAC OS, apa itu ZMQ? ZMQ merupakan sebuah library yang dirancang khusus untuk bertransfer data dengan performa tinggi secara asyncronus. Singkat kata ZMQ ini adalah library yang akan kita gunakan untuk mempush notifikasi/chat ke websocket melalui sebuah API (server-side).

Codedoct menggunakan XAMPP sebagai web server di komputer lokal.
Oke langsung saja kita setup ZMQ-nya,

Pertama install ZMQ terlebih dahulu dengan cara,
$ brew install zmq
Selanjutnya binding php-zmq
$ cd
$ git clone git://github.com/mkoppanen/php-zmq.git
$ cd php-zmq
$ phpize && ./configure
$ make && make install
Kemudian tambahkan extension zmq.so ke file php.ini yang ada di path /xamppfiles/etc/ seperti ini,
[PHP]
extension=zmq.so
;;;;;;;;;;;;;;;;;;;
; About php.ini   ;
;;;;;;;;;;;;;;;;;;;

Oke kita sudah membinding zmp ke php apache yang ada di local komputer tapi apakah pada CLI nya sudah? cek dengan cara buat sebuah file dengan nama phpinfo.php pada htdocs dan browse file tersebut pada browser sehingga akan muncul gambar seperti dibawah,


Coba cari "zmq", jika tidak ditemukan berarti zmq belum terinstall pada CLI, cara menginstallnya mudah, pada langkah diatas kita sudah membinding php-zmq masuk kembali ke folder home tempat menginstal zmq, di folder tersebut kita akan menemukan folder dengan nama php-zmq masuk ke folder modules dan copy file zmq.so,


Kemudian letakkan di path extension php CLI pada XAMPP, untuk mengetahui dimana letak path extension php CLI dapat dilakukan dengan cara browse kembali phpinfo.php dan cari "extension" sehingga akan tampak seperti ini,


Nah sekarang copy kan file zmq.so tadi ke path tersebut,


Terakhir coba refresh kembali halaman phpinfo.php, kemudian cari kembali "zmq" dan kita akan menemukannya, yang berarti ZMQ sudah berhasil terinstall dengan baik di php CLI.


===DONE!===

Ubuntu - Deploy websocket server Laravel in EC2(AWS) part-2


Melanjutkan tutorial sebelumnya kita sudah mensetting port pada EC2 server dan menjalankan serverpilot. Nah tibalah pada masalah secure website yang menggunakan "https", ada dua cara untuk mengatasi masalah ini.

Yang pertama dengan menyediakan secure websocket server dengan code seperti dibawah,
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\EventLoop\Factory;
use React\Socket\SecureServer;
use React\Socket\Server;
use App\Http\Controllers\WebSocketController;
class WebSocketSecureServer extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'websocketsecure:init';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $loop   = Factory::create();
        $webSock = new SecureServer(
            new Server('0.0.0.0:8091', $loop),
            $loop,
            array(
                'local_cert'        => 'C:/xampp/apache/conf/ssl.crt/server.crt', // path to your cert
                'local_pk'          => 'C:/xampp/apache/conf/ssl.key/server.key', // path to your server private key
                'allow_self_signed' => TRUE, // Allow self signed certs (should be false in production)
                'verify_peer' => FALSE
            )
        );
        // Ratchet magic
        $webServer = new IoServer(
            new HttpServer(
                new WsServer(
                    new WebSocketController()
                )
            ),
            $webSock
        );
        $loop->run();
    }
}

Tapi saya tidak menganjurkan menggunakan cara diatas dikarenakan cara itu hanya berlaku untuk websocket yang menggunakan ratchet jika suatu saat kita akan menggunakan websocket lain maka kita harus mensetup untuk secure servernya juga, sangat merepotkan bukan?. Maka dari itu codedoct menyarankan untuk mensetting langsung pada servernya dengan cara mengalihkan secure connection "wss" ke unsecure connection "ws" dengan menggunakan server proxy.

Oke kita mulai saja setup secure connectionnya,
Pertama, silahkan enabled modul proxy nya terlebih dahulu dengan cara,
$ sudo a2enmod proxy
$ sudo a2enmod proxy_http
$ sudo service apache2 restart

Kemudian, karna saya menggunakan apache2 untuk menjalankan server web yang saya miliki maka buka apache.conf dengan cara,
$ vim /etc/apache2/apache2.conf

Selanjutnya tambahkan code dibawah pada baris paling atas,
ProxyRequests off
SSLProxyEngine on
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off

ProxyPass /wss2/ ws://<ip public>:8080/

Setelah itu ubah connection websocket pada halaman client menjadi seperti ini,
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>user 1</title>
 </head>
 <body>
  <button onclick="register()">Register</button><br>
  <button onclick="send()">Send</button>

  <script>
   var conn = new WebSocket('wss://<url domain>/wss2/');

   conn.onopen = function(e) {
      console.log("Connection established!");
      setInterval(register, 60000);//1minute keep connection
   };

   conn.onmessage = function(e) {
      console.log(e.data);
   };

   function register() {
    conn.send(JSON.stringify({command: "register", userId: "1"}));
   }

   function send() {
    conn.send(JSON.stringify({command: "message", to: "2", from: "1", data: {message: "halo user 2"}}));
   }
  </script>
 </body>
</html>

Dan untuk mengetestnya kita tidak bisa menggunakan IP public dari server EC2 yang kita miliki karena jika dipaksa untuk mengakses dengan https maka secure connection websocket akan error karena ketidak mampuan dari server yang di akses melalui IP public untuk melakukan self-signed certificates.

Untuk itu kita harus menggunakan domain yang telah di integrasikan dengan sebuah CDN dalam hal ini codedoct menggunakan cloudflare bisa baca disini untuk setupnya.

Terakhir silahkan akses kembali page websocket anda menggunakan url domain anda dengan secure connection "https".

===DONE!===

Ubuntu - Deploy websocket server Laravel in EC2(AWS) part-1


Pada tutorial sebelumnya kita sudah membuat sebuah engine chat/notifikasi menggunakan ratchet laravel, tutorial kali ini kita akan mendeploy engine tersebut ke server.

Server yang digunakan adalah EC2 instance dari AWS dengan OS Ubuntu 16.04.

Oke langsung saja kita mulai eksperimennya, pertama silahkan deploy terlebih dahulu project websocket kita ke server jika sudah coba jalankan websocket pada server dengan cara seperti di tutorial sebelumnya,
$ php artisan websocket:init
Dan coba akses di IP public server  dengan port yg sudah kita set menjadi misal 8080, hasilnya pasti akan error karena pada AWS kita harus membuka port tersebut terlebih dahulu agar bisa di akses dari public caranya,

Masuk ke halaman service EC2,


Kemudian pada bagian bawah halaman akan terlihat bagian security groups seperti ini,


Klik launch-wizard-2 karena itu merupakan settingan yang codedoct pakai, maka akan terlihat seperti gambar dibawah,


Buka tab Inbound seperti diatas kemudian klik button edit, dan akan muncul popup seperti gambar dibawah,


Tambahkan Custom TCP dan isi seperti baris paling bawah pada gambar diatas dan terakhir klik button "save"

Sekarang buka kembali page IP public kita pada browser beserta portnya dan pasti akan terbuka, selanjutnya apakah kita akan menjalankan websocket pada terminal ssh terus-menerus? tentu tidak karena ssh sendiri akan time out dan server websocket kita akan mati dengan sendirinya, oleh sebab itu kita akan menjalankan server websocket pada background processing menggunakan "supervisor" caranya mudah,

Pertama, install terlebih dahulu supervisornya,
$ sudo apt-get install supervisor
$ sudo service supervisor restart

Selanjutnya buat sebuah folder / directory pada server sebagai tempat menyimpan file configurasi supervisor kita, kita akan buat folder baru dengan nama supervisor pada path /var/www/ kemudian pada folder tersebut buat file bernama websocket.conf dengan cara,
$ vim websocket.conf

Kemudian isi file tersebut dengan code berikut, untuk beberapa sintax ubuntu bisa lihat disini,
[unix_http_server]
file = /tmp/supervisor.sock

[supervisord]
logfile          = ./logs/supervisord.log
logfile_maxbytes = 50MB
logfile_backups  = 10
loglevel         = info
pidfile          = /tmp/supervisord.pid
nodaemon         = false
minfds           = 1024
minprocs         = 200

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl = unix:///tmp/supervisor.sock

[program:ratchet]
command                 = php artisan websocket:init
directory               = /var/www/project-websocket
process_name            = Ratchet
numprocs                = 1
autostart               = true
autorestart             = true
user                    = root
stdout_logfile          = ./logs/info.log
stdout_logfile_maxbytes = 1MB
stderr_logfile          = ./logs/error.log
stderr_logfile_maxbytes = 1MB

Terakhir, jalankan websocket.conf dengan supervisor,
$ sudo supervisord -c websocket.conf
Jika tidak ada error muncul silahkan keluar dari ssh server dan coba test kembali websocket project kita dengan mengakses IP public beserta portnya, hasilnya websocket server tetap running meskipun ssh sudah tertutup.

===DONE===

Engine - Notifikasi/chat laravel websocket using Ratchet part-2


Pada tutorial sebelumnya kita sudah membuat handling untuk websocket dari sisi server, tutorial kali ini kita akan membuat handling dari sisi client dimana client akan bertindak sebagai penerima pesan atau notifikasi dari websocket dan juga mampu untuk mengirim pesan dan notifikasi ke server websocket.

Oke kita mulai saja eksperimennya, pertama buka kembali project laravel yang sudah kita buat sebelumya, kemudian kita akan membuat 4 route baru untuk 4 url yang akan kita gunakan untuk testing websocket ini dengan mengedit file web.php pada path routes/
<?php

/*
|--------------------------------------------------------------------------
| 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::group(array('prefix' => 'test/chat'), function() {
  Route::get('user_1', function() {
    return View::make('test.chat.user-1');
  });
  Route::get('user_2', function() {
    return View::make('test.chat.user-2');
  });
  Route::get('user_admin', function() {
    return View::make('test.chat.user-admin');
  });
  Route::get('user_vendor', function() {
    return View::make('test.chat.user-vendor');
  });
});

Route::get('/', function () {
    return view('welcome');
});

Setelah itu kita akan membuat UI atau tampilannya dengan membuat 4 file baru pada path resources/views/test/chat/ masing-masing file tersebut:

user-1.blade.php
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>user 1</title>
 </head>
 <body>
  <button onclick="register()">Register</button><br>
  <button onclick="send()">Send</button>

  <script>
   var conn = new WebSocket('ws://0.0.0.0:8080');

   conn.onopen = function(e) {
      console.log("Connection established!");
      setInterval(register, 60000);//1minute keep connection
   };

   conn.onmessage = function(e) {
      console.log(e.data);
   };

   function register() {
    conn.send(JSON.stringify({command: "register", userId: "1"}));
   }

   function send() {
    conn.send(JSON.stringify({command: "message", to: "2", from: "1", data: {message: "halo user 2"}}));
   }
  </script>
 </body>
</html>

user-2.blade.php
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>user 2</title>
 </head>
 <body>
  <button onclick="register()">Register</button><br>
  <button onclick="send()">Send</button><br>

  <script>
   var conn = new WebSocket('ws://0.0.0.0:8080');

   conn.onopen = function(e) {
      console.log("Connection established!");
      setInterval(register, 60000);//1minute keep connection
   };

   conn.onmessage = function(e) {
      console.log(e.data);
   };

   function register() {
    conn.send(JSON.stringify({command: "register", userId: "2"}));
   }

   function send() {
    conn.send(JSON.stringify({command: "message", to: "1", from: "2", data: {message: "halo user 1"}}));
   }
  </script>
 </body>
</html>

user-admin.blade.php
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>user admin</title>
 </head>
 <body>
  <button onclick="register()">Register</button><br>
  <button onclick="send()">Send to user</button><br>
  <button onclick="sendA()">Send to global (khusus admin)</button><br>

  <script>
   var conn = new WebSocket('ws://0.0.0.0:8080');

   conn.onopen = function(e) {
      console.log("Connection established!");
      setInterval(register, 60000);//1minute keep connection
   };

   conn.onmessage = function(e) {
      console.log(e.data);
   };

   function register() {
    conn.send(JSON.stringify({command: "register", userId: "3"}));
   }

   function send() {
    conn.send(JSON.stringify({command: "message", to: "1", from: "3", data: {message: "halo user 1"}}));
   }

   function sendA() {
    conn.send(JSON.stringify({command: "sendAll", data: {message:"halo global"}, from: "3"}));
   }
  </script>
 </body>
</html>

user-vendor.blade.php
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>user vendor</title>
 </head>
 <body>
  <button onclick="register()">Register</button><br>
  <button onclick="send()">Send to user</button><br>
  <button onclick="sendC()">Send to connection (khusus vendor)</button>

  <script>
   var conn = new WebSocket('ws://0.0.0.0:8080');

   conn.onopen = function(e) {
      console.log("Connection established!");
      setInterval(register, 60000);//1minute keep connection
   };

   conn.onmessage = function(e) {
      console.log(e.data);
   };

   function register() {
    conn.send(JSON.stringify({command: "register", userId: "4"}));
   }

   function send() {
    conn.send(JSON.stringify({command: "message", to: "2", from: "4", data: {message: "halo user 2"}}));
   }

   function sendC() {
    conn.send(JSON.stringify({command: "sendConnection", data: {message: "halo kampret - kampret"}, to: [1,2]}));
   }
  </script>
 </body>
</html>

Jika sudah selesai semua saatnya kita test saya akan test dengan 3 browser yang berbeda yaitu chrome, safari, dan firefox dan hasilnya akan tampak seperti ini,


Cara ngetestnya mudah pertama klik tombol register dulu untuk masing-masing page, ini berguna untuk mendaftarkan ID resources atau ID dari masing-masing page pada websocket, agar page tersebut dapat dikirimi chat atau notifikasi dari suatu user.

Pada user admin terdapat tombol "Send to global (khusus admin)" ini berguna untuk mengirim sebuah chat atau notifikasi kepada semua page yang sudah terdaftar dengan mengklik tombol register tadi, sedangkan tombol "Send to connection (khusus vendor)" digunakan untuk mengirim chat atau notifikasi kepada group tertentu dengan memberikan array user id yang terdaftar.

===DONE!===

Engine - Notifikasi/chat laravel websocket using Ratchet part-1


Tidak terasa sudah lebih dari 2 bulan tidak menulis akhirnya diberi kesempatan lagi untuk berkarya. Pada artikel kali ini codedoct akan membagikan cara membuat sebuah chat engine menggunakan websocket dengan teknologi ratchet yang akan dikombinasikan dengan laravel.

Sebelum memulai codedoct akan memberitahu teknologi apa saja yang akan digunakan secara detail,
  1. Laravel versi 5.4
  2. PHP versi 5.6
  3. Ratchet cboden versi 0.4
Oke sekarang saatnya kita mulai, Pertama install terlebih dahulu ratchetnya pada laravel dengan cara,
$ composer require cboden/ratchet

Selanjutnya, buat file command laravel untuk dijalankan di server dengan cara,
$ php artisan make:command WebSocketServer --command=websocket:init

Syntax di atas akan secara otomatis membuat file baru dengan nama WebSocketServer.php pada path app/Console/Commands/ edit file tersebut sehingga akan tampak seperti ini,
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\EventLoop\Factory;
use App\Http\Controllers\WebSocketController;

class WebSocketServer extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'websocket:init';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->info("start server");
        $server = IoServer::factory(
             new HttpServer(
                 new WsServer(
                     new WebSocketController()
                 )
             ),
             8080
        );
        $server->run();
    }
}

Terlihat pada code diatas kita menambahkan file WebSocketController.php terserah kalian meletakkannya dipath mana saja, dalam hal ini codedoct meletakkan pada path app\Http\Controllers\, silahkan edit file terbut sehingga akan tampak seperti ini,
<?php

namespace App\Http\Controllers\Thirdparty;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
/**
 * @author Mihawk | codedoct.com
 */
class WebSocketController implements MessageComponentInterface
{
    protected $clients;
    private $users;
    private $userresources;
    public function __construct()
    {
        $this->clients = new \SplObjectStorage;
        $this->users = [];
        $this->userresources = [];
    }
    /**
     * [onOpen description]
     * @method onOpen
     * @param  ConnectionInterface $conn [description]
     * @return [JSON]                    [description]
     * @example connection               var conn = new WebSocket('ws://localhost:8090');
     */
    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);
        $this->users[$conn->resourceId] = $conn;
    }
    /**
     * [onMessage description]
     * @method onMessage
     * @param  ConnectionInterface $conn [description]
     * @param  [JSON.stringify]          $msg  [description]
     * @return [JSON]                    [description]
     * @example sendAll                  conn.send(JSON.stringify({command: "sendAll", data: {message:"halo global"}, from: "3"}));
     * @example sendConnection           conn.send(JSON.stringify({command: "sendConnection", data: "halo kampret - kampret", to: [1,2]}));
     * @example message                  conn.send(JSON.stringify({command: "message", to: "1", from: "9", message: "it needs xss protection"}));
     * @example register                 conn.send(JSON.stringify({command: "register", userId: 9}));
     */
    public function onMessage(ConnectionInterface $conn, $msg)
    {
        $data = json_decode($msg);
        if (isset($data->command)) {
            switch ($data->command) {
                case "sendAll":
                    if ($data->from) {
                        foreach ($this->userresources as $key => $resources) {
                            if ($key != $data->from) {
                                foreach ($resources as $resourceId) { //setiap user ID bisa buka di banyak browser
                                    if (isset($this->users[$resourceId])) {
                                        $this->users[$resourceId]->send(json_encode($data->data));
                                    }
                                }
                            }
                        }
                    }
                break;
                case "sendConnection":
                    if (count($data->to)>0) {
                        foreach ($this->userresources as $key => $resources) {
                            if (in_array($key, $data->to)) {
                                foreach ($resources as $resourceId) { //setiap user ID bisa buka di banyak browser
                                    if (isset($this->users[$resourceId])) {
                                        $this->users[$resourceId]->send(json_encode($data->data));
                                    }
                                }
                            }
                        }
                    }
                break;
                case "message":
                    if ($data->from && $data->to) {
                        if ( isset($this->userresources[$data->to]) ) {
                            foreach ($this->userresources[$data->to] as $key => $resourceId) {
                                if ( isset($this->users[$resourceId]) ) {
                                    $this->users[$resourceId]->send(json_encode($data->data));
                                }
                            }
                        }
                        if (isset($this->userresources[$data->from])) {
                            foreach ($this->userresources[$data->from] as $key => $resourceId) {
                                if ( isset($this->users[$resourceId])  && $conn->resourceId != $resourceId ) { //jika buka di browser yg berbeda
                                    $this->users[$resourceId]->send(json_encode($data->data));
                                }
                            }
                        }
                    }
                break;
                case "register":
                    if (isset($data->userId)) {
                        if (isset($this->userresources[$data->userId])) {
                            if (!in_array($conn->resourceId, $this->userresources[$data->userId]))
                            {
                                $this->userresources[$data->userId][] = $conn->resourceId;
                            }
                        }else{
                            $this->userresources[$data->userId] = [];
                            $this->userresources[$data->userId][] = $conn->resourceId;
                        }
                    }
                    // $conn->send(json_encode($this->users));
                    $conn->send(json_encode($this->userresources));
                break;
                default:
                    $example = array(
                        'methods' => [
                                    "send to all" => '{command: "sendAll", data: {message:"halo global"}, from: "3"}',
                                    "send to group" => '{command: "sendConnection", data: "halo kampret - kampret", to: [1,2]}',
                                    "message" => '{command: "message", to: "1", from: "2", data: {message:"halo gan"}',
                                    "register" => '{command: "register", userId: 9}',
                                ],
                    );
                    $conn->send(json_encode($example));
                break;
            }
        }
    }
    public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} has disconnected\n";
        unset($this->users[$conn->resourceId]);
        foreach ($this->userresources as &$userId) {
            foreach ($userId as $key => $resourceId) {
                if ($resourceId==$conn->resourceId) {
                    unset( $userId[ $key ] );
                }
            }
        }
    }
    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }
}

Code websocket server diatas akan kita panggil melalui command php artisan untuk itu kita harus menambahkannya pada kernel laravel, yang terletak pada path app/Console/ dengan nama file Kernel.php edit bagian command menjadi seperti ini,
/**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        Commands\WebSocketServer::class,
    ];

Oke jika semua sudah selesai saatnya kita coba run WebSocketServer nya dengan cara,
$ php artisan websocket:init

Sehingga akan tampak seperti ini tampilannya,


===DONE!===

What is - Webpack


Webpack merupakan sebuah tool yang berfungsi sebagai module bundler untuk mem-pack semua resource yang diperlukan oleh sebuah website.

Jika tanpa webpack kita harus menginputkan semua module file baik itu js atau css satu persatu dengan webpack hal tersebut sudah tidak perlu dilakukan lagi, cukup menginputkan satu file js atau css maka semua sudah terselesaikan.

Tidak hanya itu webpack juga berfungsi untuk mengubah ES6 menjadi format ES5 sehingga bisa dibaca oleh browser pada umunya, selain itu bisa mengubah preprocessor CSS seperti SASS, LESS, Stylus, dan sejenisnya menjadi sebuah file CSS yang bisa dibaca di browser.

Oke langsung saja kita mulai prakteknya, sebelumnya pastikan device sudah terinstall npm, jika sudah bisa lanjutkan ke bawah,
Pertama, install webpack dengan cara berikut,
$ mkdir webpack-tutorial
$ cd webpack-tutorial
$ npm init -y
$ npm install webpack

Selanjutnya, buat folder baru dengan nama "js" sehingga akan tampak struktur file seperti pada gambar dibawah,


Tambahkan script untuk menjalankan webpack dengan mengedit file package.json menjadi seperti ini,
{
  "name": "webpack-tutorial",
  "version": "1.0.0",
  "description": "basic webpack tutorial",
  "main": "index.html",
  "scripts": {
   "dev": "webpack --mode development",
   "build": "webpack --mode production",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "codedoct.com",
  "license": "ISC",
  "dependencies": {
    "webpack": "^4.16.5"
  },
  "devDependencies": {
    "webpack-cli": "^3.1.0"
  }
}


Setelah itu install kembali npm dengan mengetikan sintax "npm install" pada path project di terminal.


Pada file webpack.config.js isikan code berikut,
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');

module.exports = {
  context: __dirname,
  devtool: debug ? "inline-sourcemap" : null,
  entry: "./js/codedoct.js",
  output: {
    path: __dirname + "/js",
    filename: "codedoct.min.js"
  },
  plugins: debug ? [] : [
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
  ],
};

Dan pada file index.html isikan code berikut,
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Tutorial Webpack</title>
 </head>

 <body>
  <h1>Hello Codedocters</h1>
  <div id="number">0</div>
  <button onclick="decreaseNumber()">-</button> || <button onclick="increaseNumber()">+</button>

  <script src="js/module1.js"></script>
  <script src="js/module2.js"></script>
 </body>
</html>

Buat file baru dengan nama module1.js dan module2.js kemudian isikan code berikut,

module1.js
window.increaseNumber = function() {
 var element = document.getElementById('number');
  var number = element.innerHTML;
  number++;
  element.innerHTML = number;
}

module2.js
window.decreaseNumber = function() {
 var element = document.getElementById('number');
  var number = element.innerHTML;
  number--;
  element.innerHTML = number;
}

Setelah itu buka project pada browser sehingga akan tampak seperti pada gambar dibawah,


Dapat dilihat pada gambar diatas, hasil dari inspektur halaman menunjukan bahwa tanpa webpack kita harus menginputkan file js satu persatu.

Selanjutnya kita akan mengaplikasikan webpack pada project tersebut,
Pertama, pada folder "js" buat file dengan nama codedoct.js dan import kedua file module1.js dan module2.js dengan code berikut,
import './module1.js';
import './module2.js';

Setelah itu jalankan webpack dengan cara, buka terminal masuk ke path project dan ketikan "npm run dev" untuk membuild dengan mode development atau "npm run build" untuk membuild dengan mode production, bedanya pada mode production file codedoct.min.js akan di compress menjadi 1 line saja.
Hasil webpack akan tampak seperti gambar dibawah ini,


Dan secara otomatis webpack akan membuat file baru dengan nama codedoct.min.js pada folder "js", sehingga struktur file akan terlihat seperti ini,


Terakhir masukan file codedoct.min.js tadi pada file index.html dengan mengubah file index.html menjadi seperti ini,
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>Tutorial Webpack</title>
 </head>

 <body>
  <h1>Hello Codedocters</h1>
  <div id="number">0</div>
  <button onclick="decreaseNumber()">-</button> || <button onclick="increaseNumber()">+</button>

  <script src="js/codedoct.min.js"></script>
 </body>
</html>

Coba test kembali pada browser maka akan tampak seperti pada gambar dibawah ini,


Dapat dilihat tampilan akan bekerja sebagaimana mestinya yang menandakan hasil webpack berjalan dengan baik, coba lihat pada inspektur halaman hanya terdapat satu file js yaitu codedoct.min.js dan website berjalan dengan baik, lakukan hal yang sama juga untuk file css.
Selamat mencoba

===DONE!===

AWS - Fasting your S3 file with cloudfront


Tahukah anda jika konten terberat pada sebuah website kebanyakan adalah gambar? Ada banyak cara untuk mengoptimize kecepatan rendering data pada sebuah website salah satu cara yang harus dilakukan yaitu dengan mempercepat rendering file gambar.

Dalam hal ini AWS sudah menyediakan sebuah layanan yang bisa membantu menangani masalah tersebut yaitu dengan menggunakan cloudfront. Apa itu cloudfront?

Cloudfront merupakan sebuah layanan web yang digunakan untuk mempercepat distribusi konten web baik itu berupa konten statis maupun dinamis seperti file html, css, js, dan image kepada user client. Cloudfront mengirimkan konten melalui jaringan pusat yang disebut lokasi tepi (edge location) yang menyediakan layanan jeda waktu terendah sehingga konten dikirimkan dengan kinerja sebaik mungkin.

Berikut cara kerja dari cloudfront sendiri:
  • Jika konten sudah berada di lokasi tepi dengan jeda waktu terendah, maka cloudfront segera mengirimkannya.
  • Jika konten tidak berada di lokasi tepi tersebut, maka cloudfront mengambilnya dari tempat asal yang telah ditetapkan seperti bucket Amazon S3.

Pada dasarnya user client dapat dengan mudah menavigasi ke sebuah url dan melihat gambar dari url tersebut. Tetapi mereka mungkin tidak tahu bahwa request mereka dialihkan dari satu jaringan ke jaringan lain melalui kumpulan jaringan interkoneksi kompleks sampai akhirnya gambar itu ditemukan.

Cloudfront mempercepat distribusi konten dengan cara merutekan setiap request user client melalui jaringan backbone AWS ke lokasi tepi yang dapat melayani konten anda dengan sangat baik. Menggunakan jaringan AWS secara dramatis mengurangi jumlah jaringan yang harus dilalui oleh pengguna anda dan meningkatkan kinerja. User client mendapatkan jeda waktu lebih rendah untuk memuat byte pertama file dan kecepatan transfer data yang lebih tinggi.

Oke sekarang sudah tau kan betapa pentingnya layanan cloudfront ini untuk memaksimalkan kinerja distribusi file S3? Sekarang kita mulai saja cara setup cloudfront untuk storage S3 kita.

Jika anda belum punya bucket S3 pada AWS bisa baca disini cara setupnya.

Pertama, pada halaman home AWS kita bisa cari pada kolom pencarian ketikan saja cloudfront dan klik untuk membukanya,


Selanjutnya, akan muncul seperti gambar dibawah,


Klik tombol "Create Distribution", dan akan muncul tampilan seperti gambar dibawah,


Klik tombol "Get Started",  dan akan muncul tampilan seperti gambar dibawah,


Isi kolom pertama dengan Amazon S3 Buckets anda, kemudian biarkan field-field yang lain secara default (jika tidak mau pusing),


Klik tombol "Create Distribution" untuk segera membuat cloudfront url nya, sehingga akan muncul seperti gambar dibawah,


Dari gambar diatas kita bisa lihat pada kolom "Domain" itu merupakan url cloudfront jadi jika status cloudfront sudah "Deployed" seperti diatas itu artinya url cloudfront sudah bisa digunakan, caranya mudah yaitu tinggal ganti url S3 lama anda dengan url cloudfront misal:
url s3               ->  https://s3.us-east-2.amazonaws.com/codedoct-bucket1/image-screen.png

ganti menjadi seperti ini:
url cloudfront  ->  http://d2fm70f5trvewh.cloudfront.net/image-screen.png

Sangat mudah bukan?

===DONE!===
  

Laravel - Starter kit laravel project


Pada artikel kali ini codedoct akan memberikan dokumentasi dari project laravel yang baru saja codedoct buat yaitu sebuah project starter kit bagi anda yang ingin membuat project backend (API) laravel dengan mudah.

Clone project disini, instalasi bisa baca langsung pada readme. Bagi yg belum familiar dengan bagaimana cara clone project bisa baca disini.

Oke sekarang codedoct akan jelaskan apa saja featurenya dan cara menggunakannya.

1. Auth with RBAC (Role Base Access Controll)

Project laravel starter kit tersebut sudah support RBAC yaitu access api route dengan role sebagai dasar hak akses, misal api route get users hanya bisa di akses oleh superadmin,
Route::get('', 'UserController@getUsers')->middleware('auth.privilage:qwe123');
Jika ingin menambah multiple akses maka code akan menjadi seperti ini,
Route::get('', 'UserController@getUsers')->middleware('auth.privilage:qwe123|zxc123');
pada project sudah terdapat 2 role superadmin dengan hash_id "qwe123" dan user dengan hash_id "zxc123", anda bisa menambah sendiri user role lainnya pada table role. Sehingga anda jika kita mengakses api yang dengan role yang tidak di setujui maka akan keluar error handling,


2. Error handling

Project ini sudah mendukung berbagai error handling yang dapat di lihat pada /app/Exceptions/Handler.php

3. Doctrine migration

Untuk memudahkan dalam manajemen table maka digunakan doctrine yang di padukan dengan eloquent laravel.

4. Support payment with midtrans

Project ini sudah didukung untuk pembayaran payment gateway dengan midtrans, dimana dari client apps (ios atau android) dapat mengakses api "charge" untuk generate token yang kemudian digunakan untuk mengakses midtrans payment.


5. Support notification with Firebase

Project ini sudah mendukung push notification menggunakan firebase ke semua client dengan cara daftarkan dulu device token setiap client menggunakan api,


kemudian anda bisa push notification ke semua user atau ke user tertentu yang sudah didaftarkan device tokennya,


6. Storage file with Server or S3(AWS)

Project ini sudah mendukung penyimpanan server  atau s3 yang dapat anda atur pada file .env dengan syntax yang sangat sederhana karena sudah di handle pada ImageServices.php
if ($request->file('image')) {
  // upload image
  $uploadImage = $imageService->uploadImage($request->file('image'), 'profile', $user, 800);

  $user->image = $uploadImage;
}

7. Indonesian geograph

Sudah terdapat 3 api yang dapat memudahkan anda mendapatkan data provinsi, kota, dan kecamatan.

8. Can access API with login or public

Anda dapat mengatur API mana saja yang bisa di akses secara public, wajib, login atau keduanya. dimana anda hanya tinggal menambahkan apinya pada routes general yang ada pada file /routes/General/general.php. Akses public ini sudah di handle pada file /app/Http/Middleware/AuthenticateUserSession.php

Semua api pada postmant bisa di download disini dan tinggal di import saja pada postman anda.

===DONE!===

AWS - Manage (CRUD) S3 on laravel


Pada tutorial sebelumnya kita sudah membuat bucket S3 AWS, dan kita sudah berhasil mengupload gambar secara langsung ke S3 sekaligus membuka akses globalnya sehingga gambar bisa dibuka secaral global dengan browse path url nya.

Pada tutorial kali ini codedoct akan memberikan tutorial tentang cara memanage bucket S3 kita melalui website yang menggunakan framework Laravel, dalam hal ini kita akan mengintegrasikan S3 dengan Laravel.

Oke sebelum memulai tutorial ini pastikan Anda sudah expert dalam pemrograman PHP on Laravel jika belum silahkan ikuti tutorialnya terlebih dahulu, klik disini.

Pertama, requirement yang akan kita gunakan adalah,
  • PHP => 5.6
  • Laravel => 5.4
Buka project laravel anda, kemudian install package aws pada laravel, package yang akan kita gunakan adalah league/flysystem-aws-s3-v3, jika laravel anda sudah terinstall versi yang lain maka diremove saja terlebih dahulu karena laravel 5.1 keatas support pada flysystem-aws-s3 versi V3.
Berikut cara remove dan install,
//untuk hapus versi lama
composer remove league/flysystem-aws-s3-v2

//install versi V3
composer require league/flysystem-aws-s3-v3:~1.0

Jika sudah berhasil menambahkan package aws selanjutnya, install juga image intervention untuk mengedit image yang di upload dari sisi server,
php composer.phar require intervention/image

Kemudian buat User IAM pada AWS dengan cara,
Masuk ke akun AWS anda terlebih dahulu dan masuk ke service IAM,


Selanjutnya, akan muncul page seperti gambar dibawah,


Klik button Add user untuk membuat user baru yang akan diperuntukan untuk codedoct-bucket1 yang sudah kita buat, dan akan muncul step pembuatan user seperti pada gambar dibawah,


Isi seperti gambar diatas dengan User name* terserah anda, dan jika sudah klik button "Next:Permissions",


Pada gambar diatas klik menu "Attach existing polices directly" dan klik button "Create policy" yang akan redirect halaman baru untuk membuat policy custom khusus untuk bucket codedoct-bucket1,


Pada gambar diatas ini field Service dengan S3 dan centang "All S3 actions (s3:*)", selanjutnya pada field Resources akan muncul seperti gambar dibawah,


Pilih Specific, pada field bucket klik "Add ARN" dan muncul seperti pop up pada gambar diatas,
isi field Bucket name dengan nama bucket yang sudah kita buat yaitu codedoct-bucket1, dan klik button "Add",


Pada field Object klik "Add ARN" kemudian isi pop up nya seperti pada gambar diatas, klik button"Add" sehingga akan tampak seperti gambar dibawah,


Klik button "Review policy",  dan isi field Name dan Description seperti pada gambar dibawah,


Klik button "Create policy", kemudian kembali pada halaman Set permission sebelumnya, cari policy codedoct yang baru dibuat, centang dan klik button "Next: Review" seperti gambar dibawah,


Dan akan muncul page review seperti gambar dibawah,


Klik button "Create user", dan akan muncul seperti gambar dibawah,


Simpan Access key ID dan Secret access key untuk nanti digunakan pada .env Laravel,
Untuk mengetes apakah User IAM sudah terintegrasi dengan bucket S3 atau belum coba buka service S3 dan buka "Permissions" kita akan melihat pada list pertama "Access for your AWS account" terdapat satu akun,


Terakhir kembali pada laravel, tambahkan pada .env parameter AWS dan pendukungnya,
STORAGE_FILE=s3

AWS_KEY= isi dengan Access key ID
AWS_SECRET= isi dengan Secret access key
AWS_REGION=us-east-1
AWS_BUCKET=codedoct-bucket1

Sekarang laravel anda sudah siap mengupload file pada bucket S3 AWS, untuk code laravelnya bisa dicontoh code dibawah,
<?php
namespace App\Http\Services;

use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManagerStatic;

class ImageService
{
    public function uploadImage($file, $folder_path, $for, $size)
    {
        $img = ImageManagerStatic::make($file);
        if ($img->getWidth()) {
            $img->resize($size, null, function ($constraint) {
                $constraint->aspectRatio();
            });
        }
        $img->stream();

        if (env('STORAGE_FILE', 'server')=='s3') {
            $path = 'images/'.$folder_path.'/'.$for->hash_id.'-'.str_random(6).'.'.$file->clientExtension();
            Storage::disk('s3')->put($path, $img->__toString());

            return $path;
        }

        return null;
    }

    public function deleteFile($file)
    {
        if (env('STORAGE_FILE', 'server')=='s3') {
            if (Storage::disk('s3')->has($file)) {
                Storage::disk('s3')->delete($file);
            }
        }

        return true;
    }

    public function getFile($file)
    {
        if (env('STORAGE_FILE', 'server')=='s3') {
            return 'https://s3.amazonaws.com/'.env('AWS_BUCKET').'/'.$file;
        }
    }
}


===DONE!===