1
votes

Trying to understand how to share my session data between my App and my chat server (Ratchet). I thought using Symfony & Memcache would be easy enough but I just can't seem to get it working.

I am trying to get the user_id out of the session for when somebody sends a message to the chat it will insert the user_id into the database (Chat->onMessage).

Can somebody point me in the right direction?

Flow:

  1. config.php is included on every page
  2. When user logs into the website it executes the $login->processLogin() method
  3. I start my chat server via command line (php server.php)

config.php

<?php
use MyApp\Login;
use MyApp\Session as MySession;

# Define backslash or forward slash for *NIX and IIS systems.
define('DS', DIRECTORY_SEPARATOR);
# Attempt to determine the full-server path to the 'root' folder in order to reduce the possibility of path problems.
define('BASE_PATH', realpath(dirname(__FILE__)).DS);

# Define the complete path to the root of the domain we are at (ie. /home/user/domain.com) (does't end in a slash)
define('ROOT_PATH', $_SERVER['DOCUMENT_ROOT']);
# Define where cookies may be active. ('/' means the entire domain)
define('COOKIE_PATH', '/');
# Name sessions. (needs to be alphanumeric with no periods[.]- can't be solely digits; must contain at least one letter)
define('SESSIONS_NAME', 'SiteUser');

# Get the Session Class.
require_once BASE_PATH.'modules'.DS.'Session'.DS.'Session.php';
# Check if there is a session id set the the $sesh_id variable.
$sesh_id=((isset($sesh_id)) ? $sesh_id : NULL);
# Create a new session object, thus starting a new session.
$mysession=MySession::getInstance(NULL, NULL, NULL, $sesh_id);

Login

<?php
namespace MyApp;

use Exception;

# Make sure the script is not accessed directly.
if(!defined('BASE_PATH'))
{
    exit('No direct script access allowed');
}

# Get the User Class
require_once BASE_PATH.'modules'.DS.'Login'.DS.'User.php';

/**
 * Class Login
 *
 * The Login Class is used to login in and out users as well as checking various login privileges.
 */
class Login extends User
{
    /**
     * processLogin
     *
     * Checks if the Login has been submitted and processes it.
     *
     * @access    public
     */
    public function processLogin()
    {
        if($this->isLoggedIn()===TRUE)
        {
            header("location: main.php");
            die;
        }

        # Check if the form has been submitted.
        if($_SERVER['REQUEST_METHOD']=='POST')
        {
            try
            {
                try
                {
                    $this->setLoginSessions($this->getID(), TRUE);
                    header("location: main.php");
                }
                catch(Exception $e)
                {
                    throw $e;
                }
            }
            catch(Exception $e)
            {
                throw $e;
            }
        }
    }

    /**
     * Checks if user is logged in or not. Returns TRUE if logged in, FALSE if not.
     *
     * @return bool
     */
    public function isLoggedIn()
    {
        global $mysession;
        $symfony_session=$mysession->symfony_session;

        //if(!isset($_SESSION['user_logged_in']))
        if(!$symfony_session->has('user_id'))
        {
            # Check if we have a cookie
            if(isset($_COOKIE['cookie_id']))
            {
                try
                {
                    $this->setID($_COOKIE['cookie_id']);
                }
                catch(Exception $e)
                {
                    unset($_COOKIE['user_ip']);
                    unset($_COOKIE['athenticate']);
                    unset($_COOKIE['cookie_id']);

                    return FALSE;
                }
            }
            else
            {
                return FALSE;
            }
        }
        //elseif($_SESSION['user_logged_in']===TRUE)
        if($symfony_session->get('user_logged_in')===TRUE)
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
     * Sets the login sessions.
     *
     * @param null $user_id
     * @param null $logged_in
     * @param bool $secure
     * @throws Exception
     */
    public function setLoginSessions($user_id=NULL, $logged_in=NULL, $secure=FALSE)
    {
        global $mysession;
        $symfony_session=$mysession->symfony_session;

        # Check if the user is logged in.
        if($this->isLoggedIn()===TRUE)
        {
            if($user_id===NULL)
            {
                try
                {
                    # Get the User's data.
                    $this->findUserData();
                    $user_id=$this->getID();
                    $logged_in=TRUE;
                }
                catch(Exception $e)
                {
                    throw $e;
                }
            }
        }
        $symfony_session->set('user_id', $user_id);
        $symfony_session->set('user_logged_in', $logged_in);
        /*
        # Set the User's login sessions.
        $_SESSION['user_id']=$user_id;
        $_SESSION['user_logged_in']=$logged_in;
        */
    }
}

Session

<?php
namespace MyApp;

use Memcache;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
use Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcacheSessionHandler;

/**
 * Class Session
 *
 * The Session class is used to access and manipulate Sessions and data stored in them.
 */
class Session
{
    private static $session;
    private $message=FALSE;
    private $sessname=FALSE;
    public $symfony_session;

    /**
     * Session constructor.
     *
     * Safely calls session_start().
     * Also enables sessions to span sub domains. It names the session (which is necessary for
     * session_set_cookie_params() to work). If calling this class before setting.php, $sessname (the session name) AND
     * $cookiepath (the path for cookies) MUST be defined.
     *
     * @param null $sessname
     * @param null $cookiepath
     * @param bool $secure
     * @param null $sesh_id
     */
    public function __construct($sessname=NULL, $cookiepath=NULL, $secure=FALSE, $sesh_id=NULL)
    {
        require_once BASE_PATH.'vendor'.DS.'autoload.php';
        $memcache=new Memcache;
        $memcache->connect(DOMAIN_NAME, 11211);
        $storage=new NativeSessionStorage(array(), new Handler\MemcacheSessionHandler($memcache));
        $symfony_session=new SymfonySession($storage);

        # Check if a session ID was passed.
        if($sesh_id!==NULL)
        {
            //session_id($sesh_id);
            $symfony_session->setId($sesh_id);
        }
        # Is a session already started?
        //if(!isset($_SESSION['s_set']))
        if(!$symfony_session->has('s_set'))
        {

            # If we haven't been given a session name, we will give it one.
            if(empty($cookiepath))
            {
                # Set the default cookie path be the root of the site.
                $cookiepath=DS;
                # Check if the cookie path was defined in settings.php.
                if(defined('COOKIE_PATH'))
                {
                    # Check if the defined path is blank.
                    if(COOKIE_PATH!='')
                    {
                        # If the cookie path has been defined in settings.php, we'll use that path.
                        $cookiepath=COOKIE_PATH;
                    }
                }
            }
            //session_set_cookie_params($life, $cookiepath, '.'.DOMAIN_NAME, $secure);

            /*
             * Read the current save path for the session files and append our own directory to this path.
             * Note: In order to make that platform independent, we need to check for the file-seperator first.
             * Now we check if the directory already has been created, if not, create one.
             * Then we set the new path for the session files.
             */
            # Get the session save path.
            $save_path=session_save_path();
            # Find out if our custom_session folder exists. If not, let's make it.
            if(!is_dir(BASE_PATH.'../custom_sessions'.DS.'.'))
            {
                mkdir(BASE_PATH.'../custom_sessions', 0755);
            }
            # Is our custom_sessions folder the session save path? If not, let's make it so.
            if($save_path!==BASE_PATH.'../custom_sessions')
            {
                //session_save_path(BASE_PATH.'../custom_sessions');

                # How do I set the save path in Symfony?
            }

            # If we haven't been given a session name, we will give it one.
            if(empty($sessname))
            {
                # Set the default session name.
                $sessname='PHPSESSID';
                # Check if the session name was defined in settings.php.
                if(defined('SESSIONS_NAME'))
                {
                    # Check if the defined name is blank.
                    if(SESSIONS_NAME!='')
                    {
                        # If the session name has been defined in settings.php, we'll give the session that name.
                        $sessname=SESSIONS_NAME;
                    }
                }
            }
            $storage->setOptions(array(
                'cookie_domain'=>'.'.DOMAIN_NAME,
                'cookie_lifetime'=>0,
                'cookie_path'=>$cookiepath,
                'cookie_secure'=>$secure,
                'name'=>$sessname
            ));
            //$this->setSessname($sessname);
            # Name the session.
            //session_name($this->getSessname());

            # Session must be started before anything.
            //session_start();
            //$session->setName($this->getSessname());
            $symfony_session->start();

            # Set the s_set session so we can tell if session_start has been called already.
            //$_SESSION['s_set']=1;
            $symfony_session->set('s_set', 1);
        }

        $this->symfony_session=$symfony_session;

        print_r($symfony_session);exit;
    }

    /**
     * getSessname
     *
     * Returns the data member $sessname.
     *
     * @access    public
     */
    public function getSessname()
    {
        return $this->sessname;
    }

    /**
     * Sets the data member $sessname. If an empty value is passed, the data member will
     * be set with FALSE. Returns the set data member value.
     *
     * @param $sessname
     * @return bool
     */
    public function setSessname($sessname)
    {
        # Clean it up...
        $sessname=trim($sessname);
        # Check if the passed value is now empty.
        if(empty($sessname))
        {
            # Explicitly set the data member to false.
            $sessname=FALSE;
        }
        # Set the data member.
        $this->sessname=$sessname;

        # Return the data member after it has gone through the get method.
        return $this->getSessname();
    }

    /**
     * Gets the singleton instance of this class.
     *
     * @param null $sessname
     * @param null $cookiepath
     * @param bool $secure
     * @param null $sesh_id
     * @return Session
     */
    public static function getInstance($sessname=NULL, $cookiepath=NULL, $secure=FALSE, $sesh_id=NULL)
    {
        if(!self::$session)
        {
            self::$session=new Session($sessname, $cookiepath, $secure, $sesh_id);
        }

        return self::$session;
    }
}

server.php

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Session\SessionProvider;
use Symfony\Component\HttpFoundation\Session\Storage\Handler;
use MyApp\Chat;

$port="8080";

# Change to this directory.
chdir(dirname(__FILE__));

# Need this for the database insert.
if(!defined('DOMAIN_NAME'))
{
    define('DOMAIN_NAME', 'example.dev');
}

require_once '../../includes/lonconfig.php';
require_once '../../vendor/autoload.php';

$memcache=new Memcache;
$memcache->connect(DOMAIN_NAME, 11211);

$session=new SessionProvider(
    new Chat,
    new Handler\MemcacheSessionHandler($memcache)
);

$server=IoServer::factory(
    new HttpServer(
        new WsServer($session)
    ),
    $port,
    DOMAIN_NAME
);

$server->run();

Chat

<?php
namespace MyApp;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface
{
    protected $clients=array();

    public function onOpen(ConnectionInterface $conn)
    {
        # get the cookies
        $cookies=(string)$conn->WebSocket->request->getHeader('Cookie');
        # Returns only PHPSESSID (not SiteUser).
        //var_dump($cookies);exit;

        $this->clients[$conn->resourceId]=$conn;
        echo "New connection! ({$conn->resourceId})\n";
    }

    /**
     * @param ConnectionInterface $conn
     * @param string $data
     */
    public function onMessage(ConnectionInterface $conn, $data)
    {
        $database=$this->dbh;

        $data=json_decode($data, TRUE);

        if(isset($data['data']) && count($data['data'])!=0)
        {
            require_once BASE_PATH.'vendor'.DS.'autoload.php';
            $memcache=new Memcache;
            $memcache->connect(DOMAIN_NAME, 11211);
            $storage=new NativeSessionStorage(array(), new Handler\MemcacheSessionHandler($memcache));
            $session=new SymfonySession($storage);

            $type=$data['type'];
            $user_id=$conn->Session->get('user_id');

            $return=NULL;

            if($type=="send" && isset($data['data']['type']) && $user_name!=-1)
            {
                $msg=htmlspecialchars($data['data']['msg']);
                $date=new DateTime;
                $date->setTimezone(new DateTimeZone(TIMEZONE));

                if($data['data']['type']=='text')
                {
                    echo 'test 4';exit;
                    $database->query('SELECT `id`, `user_id`, `msg`, `type` FROM `chat` ORDER BY `id` DESC LIMIT 1', array());
                    $lastMsg=$database->statement->fetch(PDO::FETCH_OBJ);

                    if($lastMsg->user_id==$user_id && (strlen($lastMsg->msg)<=100 || strlen($lastMsg->msg)+strlen($msg)<=100))
                    {
                        # Append message.
                        $msg=$lastMsg->msg."<br/>".$msg;

                        $database->query('UPDATE `chat` SET `msg`=:msg, `posted`=NOW() WHERE `id`=:lastmsg', array(
                            ':msg'=>$msg,
                            ':lastmsg'=>$lastMsg->id
                        ));

                        $return=array(
                            "id"=>$lastMsg->id,
                            "name"=>$user_name['staffname'],
                            "type"=>"text",
                            "msg"=>$msg,
                            "posted"=>$date->format("Y-m-d H:i:sP"),
                            "append"=>TRUE
                        );
                    }
                    else
                    {
                        $database->query('INSERT INTO `chat` (`user_id`, `msg`, `type`, `posted`) VALUES (?, ?, "text", NOW())', array(
                            $user_id,
                            $msg
                        ));

                        # Get last insert ID.
                        $get_chat_id=$database->lastInsertId();
                        $return=array(
                            "id"=>$get_chat_id,
                            "name"=>$user_name['staffname'],
                            "type"=>"text",
                            "msg"=>$msg,
                            "posted"=>$date->format("Y-m-d H:i:sP")
                        );
                    }
                }

                foreach($this->clients as $client)
                {
                    $this->send($client, "single", $return);
                }
            }
            elseif($type=="fetch")
            {
                # Fetch previous messages.
                $this->fetchMessages($conn, $data['data']['id']);
            }
        }
    }
}
1

1 Answers

1
votes

You shouldn't actually share that data over all parts of your application.

If you open the tutorial of Ratchet, you will find a part about pushServers.

This allows you to push to a certain channel or $user_id(in your case)

This makes you able to not save or transfer the data in the two parts and will facilitate you to have a streamlined workflow.

I personally use the pushServer in multiple channels, they are split up into:

  • All users online (sendBroadcast)
  • All users in a group (sendGroup)
  • All users following a tag (sendTag)
  • To other users(sendToUser)

I hope this already gives you an idea on how to solve your problem, otherwise feel free to ask.