Azonnali üzenetküldés Firebase segítségével PHP-ból

Felmerült, hogy valahogy üzenetet tudjak küldeni a felhasználóknak, miközben a PHP kód egy tárhely szolgáltatónál van. Megoldás lehetne egy saját szerver valahol máshol (pl: egy 300Ft-os VPS az ARUBA-nal), amin kialakíthatnék egy javascriptes (socket.io) és PHP-s (CURL/REST) felületet, vagy csak simán átköltöztethetném az egészet oda (lévén, hogy olcsóbb mint a tárhely). A PHP-val és tárhely szolgáltatókkal az a probléma hogy nem tudok websockettel csatlakozni a szerverhez, lévén hogy a 80-as (vagy 443-as) portot az apache kisajátítja magának, más portokat meg nem fognak megnyitni.

Én annak vagyok a híve, hogy mielőtt lefejlesztünk valamit, megnézzük hogy létezik-e már az a valami. Rá is akadtam pár szolgáltatásra, ami pont azt valósítja meg, amit szeretnék. A teljesség igénye nélkül álljon itt pár közülük.

Firebase: https://www.firebase.com
PubNub: http://pubnub.com
Pusher: https://www.pubnub.com
Továbbiak…

Én a Firebase mellett ragadtam le, mert van kész angular modulja angularFire néven és mindjárt ráleltem egy composeres csomagra is (firebase-php), ami kellően egyszerű. Mivel a régi projektjeim Symfony 1.4-ben készültek, így ebben a PHP-s keretrendszerben mutatnám be.

A megoldandó probléma az volt hogy van egy felhasználó, aki a rendeléseket kell figyelje, mert esetleg vissza kell hívni az ügyfelet, vagy bármi oknál fogva jó lenne tudni hogy jött egy rendelés. Viszont egy PHP alkalmazás nem erre lett felkészítve, hiszen a felhasználó mit sem sejt abból, hogy jött egy rendelés, mindaddig, amíg nem frissíti a weboldalt, ami előtt ül. Lehet kérésekkel a háttérben ajaxosan lekérdezni, hogy jött-e új rendelés, de ez is feleslegesen terheli a szervert, amikor van a Firebase 🙂

SZERVER

A frontenden a rendelés rögzítésénél létrehozzuk az üzenetet, majd egészen addig zaklatjuk vele a felhasználót, amíg meg nem nyitja a megrendelést. Ezt a PHP részéről két sfEvent váltja ki, amit a kódban a megfelelő helyre kellett elhelyeznem.

[sourcecode language=”php”]
// event a rögzitésnel
$this->dispatcher->notify(new sfEvent($this, ‘frontend.order.new’, array(‘order’ => $order)));
// event a megnyításnál
$this->dispatcher->notify(new sfEvent($this, ‘frontend.order.open’, array(‘order’ => $order)));
[/sourcecode]

Ezekre az eventekre lehet kapcsolódni a ProjectConfiguration osztályunk setupjában:
[sourcecode language=”php”]
$this->dispatcher->connect(‘frontend.order.new’, array(‘FirebaseService’, ‘firebaseMessage’));
$this->dispatcher->connect(‘frontend.order.open’, array(‘FirebaseService’, ‘deleteFirebaseMessages’));
[/sourcecode]

Ami innen még hiányzik, az a FirebaseService osztály:

[sourcecode language=”php”]
class FirebaseService
{
const DEFAULT_URL = ‘https://kidsplace.firebaseio.com/’;
const DEFAULT_TOKEN = ‘MqL0c8tKCtheLSYcygYNtGhU8Z2hULOFs9OKPdEp’;
const DEFAULT_PATH = ‘/firebase/messages’;

static function getFireBaseInstance()
{
return new \Firebase\FirebaseLib(self::DEFAULT_URL, self::DEFAULT_TOKEN);
}

/**
* Create and save message from event object
*
* @param sfEvent $event
*/
static public function firebaseMessage(sfEvent $event)
{
$firebase = FirebaseService::getFirebaseInstance();
$message = array(
"id" => $event[‘order’]->getId(),
"message" => "Új rendelés érkezett",
"classes" => "primary"
);
$dateTime = new DateTime();
$firebase->set(self::DEFAULT_PATH . ‘/’ . $dateTime->format(‘c’), $message);
}

/**
* Delete messages with event object id
*
* @param sfEvent $event
*/
static public function deleteFirebaseMessages(sfEvent $event)
{
$firebase = FirebaseService::getFirebaseInstance();
$messages = json_decode($firebase->get(self::DEFAULT_PATH));
if (!empty($messages))
{
foreach ($messages) as $key => $message)
{
if (property_exists($message, ‘id’) && ($message->id == $event[‘order’]->getId()))
{
// ezzel a "+" karakterrel később is meg kell küzdenünk majd…
$firebase->delete(self::DEFAULT_PATH.’/’.str_replace(‘ ‘, ‘+’, $key)); //.$key
}
}
}
}
}
[/sourcecode]
Két metódusa van, az egyik létrehozza az üzenetet a megrendeléshez, a másik törli a rendeléshez tartozó üzeneteket.

Némi telepítést is igényel a szerver oldali kód, installálni kell a firebase-php-t.
[sourcecode language=”bash”]
composer require ktamas77/firebase-php dev-master
[/sourcecode]
Előtte érdemes a /lib/vendors mappába irányítani a composer telepítőjét (composer.json)
[sourcecode language=”javascript”]
{
"config": {
"vendor-dir": "lib/vendor"
},
}
[/sourcecode]
És érdemes betölteni a Projectconfigurationban a composer autoload.php fájlját is.

KLIENS
Kellett egy kliens oldali JavaScript is, ami folyamatosan figyeli a változásokat és megjeleníti az üzeneteket. Ezeknek a telepítését a bower segítségével végeztem. Érdemes létrehozni egy .bower.src fájlt, ahol megadhatjuk hová telepítse a csomagokat.
[sourcecode language=”javascript”]
{
"directory": "web/bower"
}
[/sourcecode]
majd futtassuk le a parancsokat:
[sourcecode language=”bash”]
bower init
bower install angular –save
bower install firebase –save
bower install angularfire –save
bower install angular-ui-notification –save
[/sourcecode]
Nyilván ezeket be kell emelni a layoutunkba:
[sourcecode language=”html”]
<html>
<head>
<link rel="stylesheet" href="/bower/angular-ui-notification/dist/angular-ui-notification.min.css">
<script src="/bower/angular/angular.js"></script>
<script src="/bower/angular-ui-notification/dist/angular-ui-notification.min.js"></script>
<script src="https://cdn.firebase.com/js/client/2.2.4/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/angularfire/1.2.0/angularfire.min.js"></script>
<script src="/js/app.messages.js"></script>
</head>
<body>
<span ng-app="app.message" ng-controller="MessageController"></span>

[/sourcecode]
Hiányzik még a saját angular applikációm és vezérlőm (app.messages.js):
[sourcecode language=”javascript”]
var app = angular.module("app.message", [‘firebase’, ‘ui-notification’]);
app.config(function(NotificationProvider) {
NotificationProvider.setOptions({
delay: false,
startTop: 50,
startRight: 10,
verticalSpacing: 20,
horizontalSpacing: 20,
positionX: ‘right’,
positionY: ‘top’
});
})

app.controller("MessageController", function($scope, $firebaseArray, Notification) {
var ref = new Firebase("https://kidsplace.firebaseio.com/example").child("messages");
var query = ref.orderByChild("timestamp").limitToLast(3);
// create a synchronized array
$scope.messages = $firebaseArray(query);

notifyMessage = function(message){
var link = ”;
if (message.id)
{
link = ‘<br><a href="/order/’+message.id+’/edit">’+message.id+'</a>’;
}

// replace, because "+" replaced to space on firebase server
var d = new Date(message.$id.replace(" ", "+")).addHours(-1).toLocaleString();

return Notification({
message: ‘</b>’+ message.message + ‘</b>’ + link,
title: ‘Új üzenet érkezett – ‘ + d,
}, message.type ? message.type : ‘info’)
}

notifies = {};
$scope.messages.$watch(function(event) {
var record = $scope.messages.$getRecord(event.key)
if (event.event == ‘child_added’)
{
notifies[record.$id] = notifyMessage(record);
}
else if (event.event == ‘child_removed’)
{
notifies[event.key].then(function(notificationScope){
notificationScope.kill(false)
})
}
else
console.log(event)
});
});
[/sourcecode]
Ebben, a $scope.messages változót összekötöttem a Firebase-el és figyelem ($watch), hogy jön-e új üzenet (child_added), illetve törlésre került-e valamelyik üzenet (child_removed). Szerencsémre ez akkor is megtörténik, amikor az oldal betöltődik és akkor is, amikor egy új üzenet keletkezik. Egyszerre maximum 3 üzenetet jelenítek meg (query). A megjelenítésről az angular-ui-notification komponens gondoskodik.

Remélem kedvet adtam ezzel másoknak is ahhoz, hogy használják ezeket a szolgáltatásokat. Egyelőre kis mértékű terhelés mellett ingyenesen vehetők igénybe.