Ratchet Store Connection of User & Send Message Outside of Server Instance - php

I have been following along with the tutorials here and got the ratchet server working.
My chat class is the same as the tutorial more or less at the moment, so no point in showing that here yet since my question is more about implementation strategy.
In the question I attached the user was looking how to get the connection object of a specific user. In the top answer solution keeping track of the resource IDs seems to be the way to do this.
For example when the connection is created there is this code.
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
This creates a member variable clients to store all the connections and you simply reference it now by ID to send a message. This clients however is an instance of ConnectionInterface $conn
Then to send a message you simply use the code below entering as the array key the id of the client. Very simple.
$client = $this->clients[{{insert client id here}}];
$client->send("Message successfully sent to user.");
As we know ratchet runs as a script on the server in an event loop never ending.
I'm running a Symfony project in which outside of the server instance running the ratchet code when a user does a certain action in the system I need it to send a message to a particular client connected to the server.
I'm not sure how to do this since the clients are instances of ConnectionInterface and are created when the users first connect via WebSockets. How do I send a message to a particular client in this way?
Here is a visual of what I'm trying to achieve.
References:
how to get the connection object of a specific user?

The solution I am about to post covers the entire process of communicating from server to the client on the web browser including a way to make the Websocket server run in the background (with and without docker).
Step 1:
Assuming you have ratchet installed via composer, create a folder in your project called bin and name the file "startwebsocketserver.php" (or whatever you want)
Step 2:
Copy the following code into it.
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server;
use React\EventLoop\Factory;
use WebSocketApp\Websocketserver;
use WebSocketApp\Htmlserver;
use WebSocketApp\Clientevent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Ratchet\App;
require dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/bootstrap/bootstrap.php';
$websocketserver = new Websocketserver();
$dispatcher = new EventDispatcher(); //#JA - This is used to maintain communication between the websocket and HTTP Rest API Server
$dispatcher->addListener('websocketserver.updateclient', array($websocketserver, 'updateClient'));
//// 1. Create the event loop
$loop = Factory::create();
//// 2. Create websocket servers
$webSock = new Server($loop);
new IoServer(
new HttpServer(
new WsServer( $websocketserver )
),
$webSock
);
$webSock->listen('8080', '0.0.0.0');
$app = new App( 'localhost', 6677, '0.0.0.0',$loop );
$app->route( '/', new Htmlserver(), [ '*' ] );//#JA - Allow any origins for last parameter
$app->run();
Note that in my example I am using a bootstrap file to load the database. If you are not using a database or some other method than ignore that. For the purposes of this answer I will be assuming Doctrine 2 as the database.
What this code does is creates an HTTP server & a WebSocket server within the same code base and at the same time. I'm using the $app->route approach since you can add further routing for the HTTP server to organize API Calls to talk to the WebSocket Server from your PHP Web Server.
The $loop variable includes the Websocket server in the application loop along with the HTTPServer.
Step 3:
In your project directory create a folder called websockets. Inside that create another folder called WebSocketApp. Inside that create 3 empty files for now.
Clientevent.php
Htmlserver.php
Websocketserver.php
We will go into each of these files 1 by 1 next. Failure to create these directories in this order will cause composer Autoload PSR-0 to fail to find them.
You can change the names but make sure you edit your composer file accordingly.
Step 4:
In your composer.json file make sure it looks something like this.
{
"require": {
"doctrine/orm": "^2.5",
"slim/slim": "^3.0",
"slim/twig-view": "^2.1",
"components/jquery": "*",
"components/normalize.css": "*",
"robloach/component-installer": "*",
"paragonie/random_compat": "^2.0",
"twilio/sdk": "^5.5",
"aws/aws-sdk-php": "^3.22",
"mailgun/mailgun-php": "^2.1",
"php-http/curl-client": "^1.7",
"guzzlehttp/psr7": "^1.3",
"cboden/ratchet": "^0.3.6"
},
"autoload": {
"psr-4": {
"app\\":"app",
"Entity\\":"entities"
},
"psr-0": {
"WebSocketApp":"websockets"
},
"files": ["lib/utilities.php","lib/security.php"]
}
}
In my case I'm using doctrine & slim, the important part is the "autoload" section. This section in particular is important.
"psr-0": {
"WebSocketApp":"websockets"
},
This will autoload anything in the folder websockets in the namespace of WebSocketApp. psr-0 assumed that code would be organized by folders for namespaces which is why we had to add another folder called WebSocketApp inside of websockets.
Step 5:
In the htmlserver.php file put this...
<?php
namespace WebSocketApp;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\Request;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;
class Htmlserver implements HttpServerInterface {
protected $response;
public function onOpen( ConnectionInterface $conn, RequestInterface $request = null ) {
global $dispatcher;
$this->response = new Response( 200, [
'Content-Type' => 'text/html; charset=utf-8',
] );
$query = $request->getQuery();
parse_str($query, $get_array);//#JA - Convert query to variables in an array
$json = json_encode($get_array);//#JA - Encode to JSON
//#JA - Send JSON for what you want to do and the token representing the user & therefore connected user as well.
$event = new ClientEvent($json);
$dispatcher->dispatch("websocketserver.updateclient",$event);
$this->response->setBody('{"message":"Successfully sent message to websocket server")');
echo "HTTP Connection Triggered\n";
$this->close( $conn );
}
public function onClose( ConnectionInterface $conn ) {
echo "HTTP Connection Ended\n";
}
public function onError( ConnectionInterface $conn, \Exception $e ) {
echo "HTTP Connection Error\n";
}
public function onMessage( ConnectionInterface $from, $msg ) {
echo "HTTP Connection Message\n";
}
protected function close( ConnectionInterface $conn ) {
$conn->send( $this->response );
$conn->close();
}
}
The purpose of this file is to make communication to the WebSocket server simple through basic HTTP which I will show a demo of later using cURL from the PHP Web Server. I designed this to propagate messages to the WebSocket server using Symfony's Event system and by looking at the Query String and converting it to a JSON string. It could have also been kept as an array if you wish, but in my case I needed the JSON string.
Step 6:
Next in the clientevent.php put this code...
<?php
namespace WebSocketApp;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Socket;
class Clientevent extends Event
{
const NAME = 'clientevent';
protected $user; //#JA - This returns type Entity\User
public function __construct($json)
{
global $entityManager;
$decoded = json_decode($json,true);
switch($decoded["command"]){
case "updatestatus":
//Find out what the current 'active' & 'busy' states are for the userid given (assuming user id exists?)
if(isset($decoded["userid"])){
$results = $entityManager->getRepository('Entity\User')->findBy(array('id' => $decoded["userid"]));
if(count($results)>0){
unset($this->user);//#JA - Clear the old reference
$this->user = $results[0]; //#JA - Store refernece to the user object
$entityManager->refresh($this->user); //#JA - Because result cache is used by default, this will make sure the data is new and therefore the socket objects with it
}
}
break;
}
}
public function getUser()
{
return $this->user;
}
}
Note that the User and Socket entities are entities I created from Doctrine 2. You can use whatever database you prefer. In my case I am needing to send messages to particular users from the PHP Web Server based on their login tokens from the database.
Clientevent assumes JSON string of '{"command":"updatestatus","userid":"2"}'
You can set it up however you like though.
Step 7:
In the Websocketserver.php file put this...
<?php
namespace WebSocketApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Authtoken;
use Entity\Socket;
class Websocketserver implements MessageComponentInterface {
protected $clients;
public function updateClient(Event $event)
{
$user = $event->getUser();//#JA - Get reference to the user the event is for.
echo "userid=".$user->getId()."\n";
echo "busy=".($user->getBusy()==false ? "0" : "1")."\n";
echo "active=".($user->getActive()==false ? "0" : "1")."\n";
$json["busy"] = ($user->getBusy()==false ? "0" : "1");
$json["active"] = ($user->getActive()==false ? "0" : "1");
$msg = json_encode($json);
foreach($user->getSockets() as $socket){
$connectionid = $socket->getConnectionid();
echo "Sending For ConnectionID:".$connectionid."\n";
if(isset($this->clients[$connectionid])){
$client = $this->clients[$connectionid];
$client->send($msg);
}else{
echo "Client is no longer connected for this Connection ID:".$connectionid."\n";
}
}
}
public function __construct() {
$this->clients = array();
}
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
global $entityManager;
echo sprintf('Connection %d sending message "%s"' . "\n", $from->resourceId, $msg);
//#JA - First step is to decode the message coming from the client. Use token to identify the user (from cookie or local storage)
//#JA - Format is JSON {token:58d8beeb0ada3:4ffbd272a1703a59ad82cddc2f592685135b09f2,message:register}
$json = json_decode($msg,true);
//echo 'json='.print_r($json,true)."\n";
if($json["message"] == "register"){
echo "Registering with server...\n";
$parts = explode(":",$json["token"]);
$selector = $parts[0];
$validator = $parts[1];
//#JA - Look up records in the database by selector.
$tokens = $entityManager->getRepository('Entity\Authtoken')->findBy(array('selector' => $selector, 'token' => hash('sha256',$validator)));
if(count($tokens)>0){
$user = $tokens[0]->getUser();
echo "User ID:".$user->getId()." Registered from given token\n";
$socket = new Socket();
$socket->setUser($user);
$socket->setConnectionid($from->resourceId);
$socket->setDatecreated(new \Datetime());
$entityManager->persist($socket);
$entityManager->flush();
}else{
echo "No user found from the given cookie token\n";
}
}else{
echo "Unknown Message...\n";
}
}
public function onClose(ConnectionInterface $conn) {
global $entityManager;
// The connection is closed, remove it, as we can no longer send it messages
unset($this->clients[$conn->resourceId]);
//#JA - We need to clean up the database of any loose ends as well so it doesn't get full with loose data
$socketResults = $entityManager->getRepository('Entity\Socket')->findBy(array('connectionid' => $conn->resourceId));
if(count($socketResults)>0){
$socket = $socketResults[0];
$entityManager->remove($socket);
$entityManager->flush();
echo "Socket Entity For Connection ID:".$conn->resourceId." Removed\n";
}else{
echo "Was no socket info to remove from database??\n";
}
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
This is the most complicated file to explain. To start there is a protected variable clients that stores every connection made to this ratchet websocket server. Its created in the onOpen event.
Next the onMessage event is where the web browser clients will register themselves for receiving messages. I did this using a JSON protocol. An example is in the code of the format I used in particular in which I used the token from their cookie to identify what user it was in my system along with a simple register message.
I simple look in the database in this function to see if there is an authToken to go along with the cookie.
If there is write to the Socket table in your database the $from->resourceId
This is the number that ratchet uses to keep track of that particular connection number.
Next in the onClose method note that we have to make sure to remove the entries we created when the connection closes so the database doesn't get filled with unnecessary and extra data.
Finally note that the updateClient function is a symfony Event that is triggered from the HtmlServer we did earlier.
This is what actually sends the message to the client web browser. First in case that user has many web browsers open creating different connections we loop through all known sockets related to that user. Doctrine makes this easy with $user->getSockets(), you will have to decide best way to do this.
Then you simply say $client->send($msg) to send the message to the web browser.
Step 8:
Finally in your javascript for your webbrowser put something like this.
var hostname = window.location.hostname; //#JA - Doing it this way will make this work on DEV and LIVE Enviroments
var conn = new WebSocket('ws://'+hostname+':8080');
conn.onopen = function(e) {
console.log("Connection established!");
//#JA - Register with the server so it associates the connection ID to the supplied token
conn.send('{"token":"'+$.cookie("ccdraftandpermit")+'","message":"register"}');
};
conn.onmessage = function(e) {
//#JA - Update in realtime the busy and active status
console.log(e.data)
var obj = jQuery.parseJSON(e.data);
if(obj.busy == "0"){
$('.status').attr("status","free");
$('.status').html("Free");
$(".unbusy").css("display","none");
}else{
$('.status').attr("status","busy");
$('.status').html("Busy");
$(".unbusy").css("display","inline");
}
if(obj.active == "0"){
$('.startbtn').attr("status","off");
$('.startbtn').html("Start Taking Calls");
}else{
$('.startbtn').attr("status","on");
$('.startbtn').html("Stop Taking Calls");
}
};
My demo here shows simple way to pass information back and forth with JSON.
Step 9:
To send messages from the PHP Web server I did something like this in a helper function.
function h_sendWebsocketNotificationToUser($userid){
//Send notification out to Websocket Server
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost/?command=updatestatus&userid=".$userid);
curl_setopt($ch, CURLOPT_PORT, 6677);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
}
This would attempt to send the updateStatus message for a particular user at anytime.
Step 10:
There is no step 10 you are done! Well okay not quite... To run the webserver in the background I use Docker which makes it easy. Simply execute the webserver with the following command.
docker exec -itd draftandpermit_web_1 bash -c "cd /var/www/callcenter/livesite; php bin/startwebsocketserver.php"
or something of this equivlent for your situation. Key here is the -d option I'm using which runs it in the background. Even if you run the command again it will NOT spawn two instances which is nifty. Shutting down the server is outside the scope of this but if you find a nice way to do this please amend or comment on this answer.
Also don't forget to open the ports correctly on your docker-compose file. I did something like this for my project.
ports:
- "80:80"
- "8080:8080"
- "6060:80"
- "443:443"
- "6677:6677"
#This is used below to test on local machines, just portforward this on your router.
- "8082:80"
Just remember 8080 is used by the WebSockets so it has to pass through completely.
In case you are curious about entity and database structure and what I used here is an attached image.

Related

PHP Swoole: how to make variables persistent between requests?

As a simple example, say I simply want to increment a counter when someone connects. The code I have is
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
$server = new Server("0.0.0.0", 9502);
$users = [];
$server->on("Start", function(Server $server){
echo "Swoole WebSocket Server is started at http://127.0.0.1:9502\n";
});
$server->on('Open', function(Server $server, Swoole\Http\Request $request){
global $count;
array_push($users, $request->fd);
var_dump($users);
});
Note: fd is simply the connection ID of the user (maybe short for file descriptor?), so I'm basically trying to get a simple array of all the users that have connected - the problem is though, this value isn't persistant between requests - so when the 2nd user connects, the array becomes empty again
Does anyone know of a way to fix this? I know I could use a database, but that seems very wasteful/inefficient when I want to create a realtime site - so that's why I want to store/access all the data within the php script, if possible...without using any external storage
Not tested but basicly do:
<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\Table;
$server = new Server("0.0.0.0", 9502);
$table = new Table(1024);
$table->column('user_id', Swoole\Table::TYPE_STRING, 64);
$table->create();
$server->on("Start", function(Server $server){
echo "Swoole WebSocket Server is started at http://127.0.0.1:9502\n";
});
$server->on('Open', function(Server $server, Swoole\Http\Request $request) use ($table) {
$table->set($request->fd,['user_id'=>$request->fd]);
var_dump($table->count());
});
Read more about the possibilities here: openswoole.com/docs/modules/swoole-table

Ratchet Websocket async mysql best practice

I've build a Websocket chat based on ratchet that uses reactphp async mysql and just got a couple of questions to make sure I'm doing things right, as I couldn't really find any examples for that case out there.
The use case of the Socket is a livechat that handles many different pages at the same time, about 20 pages with 1000+ Users.
Actually this works for now with 100 users, but I have doubts about the database connection to block or queue too many queries with more than thousand users at the same time.
I need to make sure the server can handle everything fast.
Ratchet: https://github.com/ratchetphp/Ratchet
Mysql: https://github.com/friends-of-reactphp/mysql
So, the Socket will be used by different applications, each of them has it's own database where the data has to be stored and published, which means there are multiple Lazy connections created, this is done by the Ratchet Pusher.
There is one database locally on the Websocket server that stores all connected interfaces in one table, out of there the connections are distributed.
The push-server starts the event loop and passes the mysql factory to the pusher constructor (push-server.php):
require dirname(__DIR__).'/websocket/vendor/autoload.php';
use React\MySQL\Factory;
use React\MySQL\QueryResult;
$loop = React\EventLoop\Factory::create();
$factory = new Factory($loop);
$pusher = new websocket01\Pusher($factory);
$webSock = new React\Socket\Server('0.0.0.0:8090', $loop);
$webServer = new Ratchet\Server\IoServer(
new Ratchet\Http\HttpServer(
new Ratchet\WebSocket\WsServer(
new Ratchet\Wamp\WampServer(
$pusher
)
)
),
$webSock
);
$loop->run();
The Pusher then creates a lazy connection for each connected Interface in the constructor, and thats my second question: Is it better to create just one async db connection at all and keep it alive (like now), or is is better to reopen the connection at every client interaction ?
(Pusher.php):
protected $connection;
protected $subconnections = array();
public function __construct($factory){
//Main Websocket db
$uri = 'localhost...';
$this->connection = $factory->createLazyConnection($uri);
//Create array with Connections to Sub dbs for every entry in the interface table
$stream = $this->connection->queryStream('SELECT * from interfaces');
$stream->on('data', function ($interface) use ($factory) {
if (!array_key_exists($interface['interface_dbhost'], $this->subconnections)) {
$uri = "".$interface['interface_dbuser'].":".$interface['interface_dbpass']."#".$interface['interface_dbhost'].":".$interface['interface_dbport']."/".$interface['interface_dbname']."";
$this->subconnections[$interface['interface_dbhost']] = $factory->createLazyConnection($uri);
}
});
$stream->on('end', function () {
echo 'Completed.';
});
}
The main (local) database will still be used to save clients data, errors, verify tokens etc.
The right database connection will then be chosen in onopen, onsubscribe, onpublish... automatically out of the subconnections array, depending on the application the client comes from, and the action he wants to perfom, for e.g:
public function onPublish(ConnectionInterface $conn, $conversation_id, $event, array $exclude, array $eligible) {
$obj = json_decode($event);
$interface = $obj->frontend;
$channel = $obj->channel;
$db = $this->selectDatabase($conversation_id, $interface); //Choose right database connection
$clienthandler = new Clienthandler\Clienthandler($this->connection);
$action = $this->processRequest($db,$obj, $conversation_id, $clienthandler, $operatorhandler, $channel);
}
I'm also passing the local db connection to the clienthandler too, as I need it there to log actions.
public function selectDatabase($conversation_id, $interface) {
if(array_key_exists($interface, $this->subconnections)) {
$dbconn = $this->subconnections[$interface];
$dbconn->ping()->then(function () {
echo 'Connection alive' . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
}
}
return $dbconn;
}
The clienthandler then saves the data and broadcasts it to all subscribers
I'm open for any advice :-)
Thank you for your help

How to get access to parameter of object, created in one daemon-like script from another daemon-like script

I'm trying to write gameserver based on Workerman.
Main idea is: list of workers accept messages from clients and puts them to the queue (RabbitMQ), another group of workers get messages from queue, do some calculation and update GameWorld instance accordingly.
GameWorld instance itself is created on start of the main process, workers are created after creating of the GameWorld object. So, I wrote dummy class Server:
namespace Server;
use \Workerman\Worker;
use \Workerman\Lib\Timer;
class Server {
private $name;
public function setName(string $name){
$this->name = $name;
}
public function printName(){
echo $this->name;
}
}
Also I wrote two simple workers just for testing concept of updating object from different workers.
First (start_worker1.php):
use \Workerman\Worker;
use \Workerman\Lib\Timer;
use \Server\Server;
global $ws_worker;
$ws_worker = new Worker('Websocket://0.0.0.0:8000');
$ws_worker->name = 'FirstWorker';
$ws_worker->onWorkerStart = function($ws_worker)
{
$ws_worker->server = new Server();
$ws_worker->server->setName("FirstName");
echo "worker1 started\n";
$ws_worker->is_started = TRUE;
var_dump($ws_worker);
};
Here I created new Server object and give it a name, also changed property $ws_worker->is_started to TRUE (default value is False).
Second(start_worker2.php):
use \Workerman\Worker;
use \Workerman\Lib\Timer;
global $ws_worker;
$worker = new Worker('Websocket://0.0.0.0:8001');
$worker->name = 'SecondWorker';
// here I'm checking, if I had an object of the first worker
// and actually it outputs all the data about first worker object,
// but $ws_worker->server is NULL and $ws_worker->is_started = FALSE
// This is confusing me so much..
var_dump($ws_worker);
// here I'm trying to detect when first worker is started
// but it return false all the time..
while(!$ws_worker->is_started){
var_dump($ws_worker->is_started);
sleep(1);
}
$worker->onWorkerStart = function() use($ws_worker){
echo "worker2 started\n";
var_dump($ws_worker);
$ws_worker->sever->setName("NewName");
};
Here I created second worker and tried to access server property of the first worker object, but with no success..
All this stuff is started this way:
use Workerman\Worker;
use Server\Server;
if(strpos(strtolower(PHP_OS), 'win') === 0)
{
exit("start.php does not support windows, please use start_for_win.bat\n");
}
if(!extension_loaded('pcntl'))
{
exit("Error! <pcntl> extension not found! Please install pcntl extension.");
}
if(!extension_loaded('posix'))
{
exit("Error! <posix> extension not found! Please install posix extension.");
}
define('GLOBAL_START', 1);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/start_worker1.php';
require_once __DIR__ . '/start_worker2.php';
// Run all services
Worker::runAll();
I guess that in worker2 I can access an worker1 object before function $ws_worker->onWorkerStart is evaluated, but I have no idea, how to access worker1 object in real time ( I mean - get access to current state of the object).
I'm new to PHP OOP style programming, I should say. So please show me where is my mistake. Detailed explanation greatly appreciated.

I am trying to implement a pulse into a PHP Ratchet Websocket Application

Hello Stack Overflow,
I am building a browser-based text only multi-player RPG written in PHP with Ratchet as the backbone.
What I have so far: It works very well. I have implemented a simple and effective command interpretor that does a good job of transferring data between the client and server. I'm able to easily perform database operations and instantiate outside classes inside my Server class to use to pass information back to the client.
Where I've gotten stuck: For some reason, my brain broke trying to implement ticks, which in the context of my game, is a set of events that happens every 45 seconds. It's basically the heartbeat of the game, and I can't move forward without having a reliable and graceful implementation of it. The tick needs to do a multitude of things, including (but not limited to): sending messages to players, updating player regen, memory handling, and so on. Generally, all these actions can be coded and placed in an Update class.
But I can't figure out how to get the tick to actually happen. The tick itself, just a function that occurs every 45 seconds inside my react loop, it should start when the server starts. It absolutely needs to be server-side. I could technically implement it client-side and sync with values in a database but I do NOT want to go down that road.
I feel like this should be easier than my brain is making it.
What I've tried:
I've tried running a simple recursive function that constructs my update class on a timer using sleep(45), but again, this needs to start when the server starts, and if I toss an infinite looping function in the construct of my server class, the startup script never gets passed that and the game never starts.
I've tried using the onPeriodicTimer function that comes with react, but I can't figure out how to implement it..
I've tried something crazy like using node js to send a message to my server every 45 seconds and my interpreter catches that particular message and starts the tick process. This is the closest I've gotten to a successful implementation but I'm really hoping to be able to do it without a client having to connect and talk to the server, it seems hackey.
I've tried ZeroMQ to achieve the same goal as above (a client that sends a message to my server that triggers the update) but again, I don't want to have to have a client listener constantly connected for the game to run, and also, zeroMQ is a lot to deal with for something so small.. I had no luck with it.
There has to be a better way to achieve this. Any help would be appreciated.
For reference, here is a basic outline of out my socket application is working. To start, I used the "Hello World" tutorial on the Ratchet website.
So I have a startup.php script that I run to initialize the Server class, which accepts messages from connected clients. onMessage, an interpretor class is instantiated which parses the message out and looks for the command the client passed in a database table which loads the corresponding Class and Method for that command, that data is based back to the onMessage function, the class and method for the command is called, and the result is passed back to the client.
TLDR: How do I add a repeating function to a Ratchet websocket server that can send messages to connected clients every 45 seconds?
Here's the Server class:
class Server implements MessageComponentInterface
{
public $clients;
public function __construct()
{
$this->clients = new \SplObjectStorage;
//exec("nodejs ../bin/java.js", $output);
}
public function onOpen(ConnectionInterface $conn)
{
$conn->connected_state = 0;
$this->clients->attach($conn);
// Initiate login
$login = new Login('CONN_GETNAME');
if($login->success)
{
$conn->send($login->output);
$conn->connected_state = $login->new_state;
$conn->chData = new Character();
}
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg)
{
if($msg == 'do_tick')
{
echo "a tick happened <br>";
}
else
{
if($from->connected_state == 'CONN_CONNECTED' || $msg == 'chardump')
{
$interpretor = new Interpret($msg);
if($interpretor->success)
{
$action_class_var = $interpretor->class;
$action_method_var = $interpretor->function;
$action_class = new $action_class_var($this->clients, $from, $interpretor->msg);
$action = $action_class->{$action_method_var}();
foreach($this->clients as $client)
{
if($action->to_room)
{
if($from != $client)
{
$client->send($action->to_room);
}
}
if($action->to_global)
{
if($from != $client)
{
$client->send($action->to_global);
}
}
if($action->to_char)
{
$client->send($action->to_char);
}
}
}
else
{
$from->send('Huh?');
}
}
else
{
$login = new Login($from->connected_state, $msg, $from);
$from->connected_state = $login->new_state;
if($login->char_data && count($login->char_data)>0)
{
foreach($login->char_data as $key=>$val)
{
$from->chData->{$key} = $val;
}
}
$from->send($login->output);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
Perhaps an onTick function added to this class that gets called every X seconds? Is that possible?
To broadcast the message to everyone in intervals of 45 seconds (or any other number), you must control the event loop which Ratchet uses.
You need to add a timed event, various vendors call this timed event, timer event, repeatable event, but it always behaves the same - a function fires after X amount of time.
Class that you are after is documented at this link
Alternatively, you can use icicle instead of Ratchet. I personally prefer it, I don't have any particular reason for the preference - both libraries are excellent in my opinion, and it's always nice to have an alternative.
Interestingly enough, you tried to use ZeroMQ - it's a transport layer and it's definitely one of the best libraries / projects I've ever used. It plays nicely with event loops, it's definitely interesting for developing distributed systems, job queues and similar.
Good luck with your game! If you'll have any other questions regarding WS, scaling to multiple machines or similar - feel free to ping me in the comments below this answer.
Thank you, N.B.!
For anyone that might be stuck in a similar situation, I hope this helps someone out. I had trouble even figuring out what terms I should be googling to get to the bottom of my problem, and as evidenced by the comments below my original question, I got flack for not being "specific" enough. Sometimes it's hard to ask a question if you're not entirely sure what you're looking for!
Here is what the game's startup script looks like now, with an implemented "tick" loop that I've tested.
<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server as Reactor;
use React\EventLoop\Factory as LoopFactory;;
require dirname(__DIR__) . '/vendor/autoload.php';
foreach(new DirectoryIterator(dirname(__DIR__) .'/src/') as $fileInfo)
{
if($fileInfo->isDot() || $fileInfo->isDir())
{
continue;
}
require_once(dirname(__DIR__) . '/src/' . $fileInfo->getFilename());
}
$clients = null;
class Server implements MessageComponentInterface
{
public function __construct(React\EventLoop\LoopInterface $loop)
{
global $clients;
$clients = new \SplObjectStorage;
// Breathe life into the game
$loop->addPeriodicTimer(40, function()
{
$this->doTick();
});
}
public function onOpen(ConnectionInterface $ch)
{
global $clients;
$clients->attach($ch);
$controller = new Controller($ch);
$controller->login();
}
public function onMessage(ConnectionInterface $ch, $args)
{
$controller = new Controller($ch, $args);
if($controller->isLoggedIn())
{
$controller->interpret();
}
else
{
$controller->login();
}
}
public function onClose(ConnectionInterface $conn)
{
global $clients;
$clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e)
{
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
public function doTick()
{
global $clients;
$update = new Update($clients);
}
}
$loop = LoopFactory::create();
$socket = new Reactor($loop);
$socket->listen(9000, 'xx.xx.xx.xxx');
$server = new IoServer(new HttpServer(new WsServer(new Server($loop))), $socket, $loop);
$server->run();

Error while importing Quickbook library in Laravel 5.3

I am integrating quickbooks with my laravel app. After integration I got this error,
PHP Warning: require_once(../QuickBooks.php): failed to open stream:
No such file or directory in
/home/vipin/projects/development/Quickbook/config/app.php on line 2
PHP Fatal error: require_once(): Failed opening required '../QuickBooks.php'
(include_path='.:/usr/share/php:/home/ubuntu/projects/development/Quickbook/vendor/consolibyte/quickbooks')
in /home/ubuntu/projects/development/Quickbook/config/app.php on line
2
Here is my controller Quickbook.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
// require_once '../QuickBooks.php';
use App\Http\Requests;
class QuickBooksController extends Controller
{
private $IntuitAnywhere;
private $context;
private $realm;
public function __construct(){
if (!\QuickBooks_Utilities::initialized(env('QBO_DSN'))) {
// Initialize creates the neccessary database schema for queueing up requests and logging
\QuickBooks_Utilities::initialize(env('QBO_DSN'));
}
$this->IntuitAnywhere = new \QuickBooks_IPP_IntuitAnywhere(env('QBO_DSN'), env('QBO_ENCRYPTION_KEY'), env('QBO_OAUTH_CONSUMER_KEY'), env('QBO_CONSUMER_SECRET'), env('QBO_OAUTH_URL'), env('QBO_SUCCESS_URL'));
}
public function qboConnect(){
if ($this->IntuitAnywhere->check(env('QBO_USERNAME'), env('QBO_TENANT')) && $this->IntuitAnywhere->test(env('QBO_USERNAME'), env('QBO_TENANT'))) {
// Set up the IPP instance
$IPP = new \QuickBooks_IPP(env('QBO_DSN'));
// Get our OAuth credentials from the database
$creds = $this->IntuitAnywhere->load(env('QBO_USERNAME'), env('QBO_TENANT'));
// Tell the framework to load some data from the OAuth store
$IPP->authMode(
\QuickBooks_IPP::AUTHMODE_OAUTH,
env('QBO_USERNAME'),
$creds);
if (env('QBO_SANDBOX')) {
// Turn on sandbox mode/URLs
$IPP->sandbox(true);
}
// This is our current realm
$this->realm = $creds['qb_realm'];
// Load the OAuth information from the database
$this->context = $IPP->context();
return true;
} else {
return false;
}
}
public function qboOauth(){
if ($this->IntuitAnywhere->handle(env('QBO_USERNAME'), env('QBO_TENANT')))
{
; // The user has been connected, and will be redirected to QBO_SUCCESS_URL automatically.
}
else
{
// If this happens, something went wrong with the OAuth handshake
die('Oh no, something bad happened: ' . $this->IntuitAnywhere->errorNumber() . ': ' . $this->IntuitAnywhere->errorMessage());
}
}
public function qboSuccess(){
return view('qbo_success');
}
public function qboDisconnect(){
$this->IntuitAnywhere->disconnect(env('QBO_USERNAME'), env('QBO_TENANT'),true);
return redirect()->intended("/yourpath");// afer disconnect redirect where you want
}
public function createCustomer(){
$CustomerService = new \QuickBooks_IPP_Service_Customer();
$Customer = new \QuickBooks_IPP_Object_Customer();
$Customer->setTitle('Ms');
$Customer->setGivenName('Shannon');
$Customer->setMiddleName('B');
$Customer->setFamilyName('Palmer');
$Customer->setDisplayName('Shannon B Palmer ' . mt_rand(0, 1000));
// Terms (e.g. Net 30, etc.)
$Customer->setSalesTermRef(4);
// Phone #
$PrimaryPhone = new \QuickBooks_IPP_Object_PrimaryPhone();
$PrimaryPhone->setFreeFormNumber('860-532-0089');
$Customer->setPrimaryPhone($PrimaryPhone);
// Mobile #
$Mobile = new \QuickBooks_IPP_Object_Mobile();
$Mobile->setFreeFormNumber('860-532-0089');
$Customer->setMobile($Mobile);
// Fax #
$Fax = new \QuickBooks_IPP_Object_Fax();
$Fax->setFreeFormNumber('860-532-0089');
$Customer->setFax($Fax);
// Bill address
$BillAddr = new \QuickBooks_IPP_Object_BillAddr();
$BillAddr->setLine1('72 E Blue Grass Road');
$BillAddr->setLine2('Suite D');
$BillAddr->setCity('Mt Pleasant');
$BillAddr->setCountrySubDivisionCode('MI');
$BillAddr->setPostalCode('48858');
$Customer->setBillAddr($BillAddr);
// Email
$PrimaryEmailAddr = new \QuickBooks_IPP_Object_PrimaryEmailAddr();
$PrimaryEmailAddr->setAddress('support#consolibyte.com');
$Customer->setPrimaryEmailAddr($PrimaryEmailAddr);
if ($resp = $CustomerService->add($this->context, $this->realm, $Customer))
{
//print('Our new customer ID is: [' . $resp . '] (name "' . $Customer->getDisplayName() . '")');
//return $resp;
//echo $resp;exit;
//$resp = str_replace('{','',$resp);
//$resp = str_replace('}','',$resp);
//$resp = abs($resp);
return $this->getId($resp);
}
else
{
//echo 'Not Added qbo';
print($CustomerService->lastError($this->context));
}
}
public function addItem(){
$ItemService = new \QuickBooks_IPP_Service_Item();
$Item = new \QuickBooks_IPP_Object_Item();
$Item->setName('My Item');
$Item->setType('Inventory');
$Item->setIncomeAccountRef('53');
if ($resp = $ItemService->add($this->context, $this->realm, $Item))
{
return $this->getId($resp);
}
else
{
print($ItemService->lastError($this->context));
}
}
public function addInvoice($invoiceArray,$itemArray,$customerRef){
$InvoiceService = new \QuickBooks_IPP_Service_Invoice();
$Invoice = new \QuickBooks_IPP_Object_Invoice();
$Invoice = new QuickBooks_IPP_Object_Invoice();
$Invoice->setDocNumber('WEB' . mt_rand(0, 10000));
$Invoice->setTxnDate('2013-10-11');
$Line = new QuickBooks_IPP_Object_Line();
$Line->setDetailType('SalesItemLineDetail');
$Line->setAmount(12.95 * 2);
$Line->setDescription('Test description goes here.');
$SalesItemLineDetail = new QuickBooks_IPP_Object_SalesItemLineDetail();
$SalesItemLineDetail->setItemRef('8');
$SalesItemLineDetail->setUnitPrice(12.95);
$SalesItemLineDetail->setQty(2);
$Line->addSalesItemLineDetail($SalesItemLineDetail);
$Invoice->addLine($Line);
$Invoice->setCustomerRef('67');
if ($resp = $InvoiceService->add($this->context, $this->realm, $Invoice))
{
return $this->getId($resp);
}
else
{
print($InvoiceService->lastError());
}
}
public function getId($resp){
$resp = str_replace('{','',$resp);
$resp = str_replace('}','',$resp);
$resp = abs($resp);
return $resp;
}
}
Config/app.php
<?php
require_once '../QuickBooks.php';
return [
'qbo_token' => env('QUICKBOOK_TOKEN'),
'qbo_consumer_key' => env('QBO_OAUTH_CONSUMER_KEY'),
'qbo_consumer_secret' => env('QBO_CONSUMER_SECRET'),
'qbo_sandbox' => env('QBO_SANDBOX'),
'qbo_encryption_key' => env('QBO_ENCRYPTION_KEY'),
'qbo_username' => env('QBO_USERNAME'),
'qbo_tenant' => env('QBO_TENANT'),
'qbo_auth_url' => 'http://app.localhost:8000/qbo/oauth',
'qbo_success_url' => 'http://app.localhost:8000/qbo/success',
'qbo_mysql_connection' => 'mysqli://'. env('DB_USERNAME') .':'. env('DB_PASSWORD') .'#'. env('DB_HOST') .'/'. env('DB_DATABASE'),
There are several areas to improve on here with the given code & approach.
As Anton correctly points out, you should not be directly requiring any of the quickbooks library files. If you've loaded this in via Composer then they will be automatically loaded because the Composer autoloader will load the QuickBooks file from the vendor. This is correct for Laravel as well as general Composer-based applications - the only difference with Laravel is that there isn't a specific Laravel Package ServiceProvider that's been written for this SDK, but that doesn't matter.
The QuickBooks library tries to jump on top of autoloading any class that starts with 'QuickBooks', so you're better off making a QuickBooks folder for your controller class. This is more of a 'gotcha' and has been pointed out in the repo issues.
The reason you're getting the Driver/.php error is because you have not specified your QBO_DSN, or have done so incorrectly - this DSN environment variable that you're passing to the initialisation is being run through parse_url() in the SDK code, coming up false or null and breaking the auto-loader for initalisation. If this was set to a proper connection string (e.g. mysqli://username:password#host:port/database and note that port must be a number or it's considered malformed), it would correctly process the DSN and continue to load the page. Be aware that initialisation will attempt to parse and fetch the network address of the host, so you can't just put a dummy value in there and expect it to work - this needs to exist first.
You're mixing your environment variables and application configuration, without using either of them properly. If you wanted your DB connection string (a.k.a. QBO_DSN) to be constructed a particular way into the application configuration setting qbo_mysql_connection, then you should be using the configuration setting when trying to initialise/load/etc. Instead of using env('QBO_DSN'), you should be using config('app.qbo_mysql_connection') to load the constructed version from your app settings. Typically you would not be loading so many environment variables into a controller at all - that should be handled by the application, and then the controller calling the application configuration so it's agnostic of how they were defined.
You shouldn't need to require anything from inside the app configuration file either - that file is just for configuration variables being set up.
Since the QuickBooks SDK isn't properly namespaced (yet), there isn't a nice PSR-4 way of loading (and use-ing) the classes, but it's still good practice to use use clauses at the top of the file (e.g. use QuickBooks_Utilities;) so that you can use the classes without fear of forgetting the preceding backslash (i.e. no more \QuickBooks_Utilities, just QuickBooks_Utilities in usage) - there are several instances in the given code where this has been forgotten, and will not work because the Laravel application is namespaced and will look for those classes in the App\Http\Controllers namespace (e.g. errors like "Cannot find class App\Http\Controllers\QuickBooks_Utilities").
Indentation - pick a style (e.g. tabs, 2-space, PSR-2, etc) and then stick to it. Run phpcs or some other clean-up tool over all of your code before committing to your repository or posting on SO - readability is important!
Using require instead of autoloader is a bad practice in modern frameworks (and generally in modern PHP). I highly recommend using the package manager (eg composer) to properly add modules to the project.
For example, to add a quickbooks library into the project using composer, you need to run only one command:
composer require consolibyte/quickbooks
Add this line in footer of Config/app.php
require_once '../QuickBooks.php';

Categories